Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions codegen/didit_codegen/render_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ def _string_sample(schema: Json, n: int) -> str:
return text


def file_sample(spec: Spec, schema: Json) -> Json:
"""The sample value of a file argument: one file, or a list of files for an array field."""
resolved = spec.resolve(schema)
if isinstance(resolved, dict) and resolved.get("type") == "array":
return [b"file-content"]
return b"file-content"


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):
Expand Down Expand Up @@ -335,15 +343,15 @@ def _kwargs_for(method: Method) -> list[tuple[Arg, Json]]:
assert isinstance(example, dict)
for key, value in example.items():
if key in method.file_fields:
value = b"file-content"
value = file_sample(spec, by_wire[key].schema)
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"
chosen[name] = file_sample(spec, by_wire[name].schema)
for arg in body_args:
if arg.wire in chosen:
out.append((arg, scrub(chosen[arg.wire], arg.wire)))
Expand Down
8 changes: 4 additions & 4 deletions openapi/SOURCE.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"verification.json": {
"url": "https://docs.didit.me/openapi-25.json",
"sha256": "12b2fdb05572755d61b93791f6964b7d883ba7b20f7ea9b31543903b6edab8a9",
"fetched_at": "2026-10-29T06:36:56Z"
"sha256": "d1783bd862848198f7e5275c28f6a1bcdd59737c0d92f72bba3f6f3cf8b8ae3e",
"fetched_at": "2026-11-05T06:33:10Z"
},
"account.json": {
"url": "https://docs.didit.me/openapi-auth.json",
"sha256": "ae1b5e07df822fe93cae27a9eaa0790948069352c9d98c87eecf7efd644c92cb",
"fetched_at": "2026-10-29T06:36:56Z"
"sha256": "5b45b1896ef3107dd9a601c76d9ba35761bae5f04ac731b6c27876603903bf05",
"fetched_at": "2026-11-05T06:33:10Z"
},
"webhook-vectors.json": {
"generator": "product-automations scripts/server-sdks/make_vectors.py (seeded 2026-10-17)",
Expand Down
295 changes: 272 additions & 23 deletions openapi/account.json

Large diffs are not rendered by default.

31,578 changes: 25,052 additions & 6,526 deletions openapi/verification.json

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion src/didit/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,22 @@ def _is_union(origin: Any) -> bool:
return origin is Union or origin is types.UnionType


def _resolve_lazy(type_: Any) -> Any:
"""A lazily imported model (a class with ``__didit_resolve__``) as the model itself."""
resolve = getattr(type_, "__didit_resolve__", None)
return resolve() if resolve is not None else type_


def construct_type(type_: Any, value: object) -> object:
"""Best-effort conversion of ``value`` to ``type_`` that never raises."""
type_ = _resolve_lazy(type_)
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)]
members = [_resolve_lazy(arg) for arg in get_args(type_) if arg is not type(None)]
for member in members:
try:
return _adapter(member).validate_python(value)
Expand Down
4 changes: 2 additions & 2 deletions src/didit/resources/address_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def validate(

**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.
**Rate limit.** Shared write budget of 300 requests per minute per API key across POST/PATCH/DELETE endpoints (transaction creates and event ingestion have a separate budget of 600).

``POST /v3/address-validation/``

Expand Down Expand Up @@ -159,7 +159,7 @@ async def validate(

**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.
**Rate limit.** Shared write budget of 300 requests per minute per API key across POST/PATCH/DELETE endpoints (transaction creates and event ingestion have a separate budget of 600).

``POST /v3/address-validation/``

Expand Down
89 changes: 87 additions & 2 deletions src/didit/resources/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
)
from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query
from didit.types.components.application_analytics_summary import ApplicationAnalyticsSummary
from didit.types.components.application_analytics_today import ApplicationAnalyticsToday
from didit.types.components.application_analytics_widget_response import (
ApplicationAnalyticsWidgetResponse,
)
Expand All @@ -35,7 +36,7 @@
class Analytics(SyncAPIResource):
"""The ``analytics`` resource of the Didit API.

Methods: ``retrieve``.
Methods: ``retrieve``, ``retrieve_today``.
"""

@cached_property
Expand Down Expand Up @@ -119,11 +120,51 @@ def retrieve(
),
)

def retrieve_today(
self,
*,
tz: 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,
) -> ApplicationAnalyticsToday:
"""Get today's verifications by hour.

Today's user verification (KYC) sessions, from local midnight in `tz`, counted per local hour they were created in, next to the same hours of the same weekday one week before, and split by the outcome each one holds now: approved, in review or declined. This is the volume card of the Business Console's Home.

`total` is today so far. `previous_total_so_far` counts the comparison day only up to the same local time, so a change between them compares the same part of both days. The hours still to come today are `0`. A day on which the clocks change has 23 or 25 hours: the missing hour stays `0` and the repeated hour holds both.

``GET /v3/organization/{organization_id}/application/{application_id}/analytics/today/``

Args:
tz: IANA time zone whose midnight starts today, for example `Europe/Madrid` or `America/Mexico_City`. Omit it for UTC.
extra_headers: Headers to add to (or, with ``None``, remove from) this request.
extra_query: Query parameters to add to this request.
extra_body: Top-level JSON fields to add to this request's body.
timeout: Overrides the client's timeout for this request, in seconds.
"""
return cast(
"ApplicationAnalyticsToday",
self._client.request(
"get",
"/v3/organization/{organization_id}/application/{application_id}/analytics/today/",
cast_to=ApplicationAnalyticsToday,
scope="application",
auth="api_key",
query={"tz": tz},
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
),
)


class AsyncAnalytics(AsyncAPIResource):
"""The ``analytics`` resource of the Didit API.

Methods: ``retrieve``.
Methods: ``retrieve``, ``retrieve_today``.
"""

@cached_property
Expand Down Expand Up @@ -207,26 +248,70 @@ async def retrieve(
),
)

async def retrieve_today(
self,
*,
tz: 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,
) -> ApplicationAnalyticsToday:
"""Get today's verifications by hour.

Today's user verification (KYC) sessions, from local midnight in `tz`, counted per local hour they were created in, next to the same hours of the same weekday one week before, and split by the outcome each one holds now: approved, in review or declined. This is the volume card of the Business Console's Home.

`total` is today so far. `previous_total_so_far` counts the comparison day only up to the same local time, so a change between them compares the same part of both days. The hours still to come today are `0`. A day on which the clocks change has 23 or 25 hours: the missing hour stays `0` and the repeated hour holds both.

``GET /v3/organization/{organization_id}/application/{application_id}/analytics/today/``

Args:
tz: IANA time zone whose midnight starts today, for example `Europe/Madrid` or `America/Mexico_City`. Omit it for UTC.
extra_headers: Headers to add to (or, with ``None``, remove from) this request.
extra_query: Query parameters to add to this request.
extra_body: Top-level JSON fields to add to this request's body.
timeout: Overrides the client's timeout for this request, in seconds.
"""
return cast(
"ApplicationAnalyticsToday",
await self._client.request(
"get",
"/v3/organization/{organization_id}/application/{application_id}/analytics/today/",
cast_to=ApplicationAnalyticsToday,
scope="application",
auth="api_key",
query={"tz": tz},
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
),
)


class AnalyticsWithRawResponse:
def __init__(self, resource: Analytics) -> None:
self._resource = resource
self.retrieve = to_raw_response_wrapper(resource.retrieve)
self.retrieve_today = to_raw_response_wrapper(resource.retrieve_today)


class AsyncAnalyticsWithRawResponse:
def __init__(self, resource: AsyncAnalytics) -> None:
self._resource = resource
self.retrieve = async_to_raw_response_wrapper(resource.retrieve)
self.retrieve_today = async_to_raw_response_wrapper(resource.retrieve_today)


class AnalyticsWithStreamingResponse:
def __init__(self, resource: Analytics) -> None:
self._resource = resource
self.retrieve = to_streamed_response_wrapper(resource.retrieve)
self.retrieve_today = to_streamed_response_wrapper(resource.retrieve_today)


class AsyncAnalyticsWithStreamingResponse:
def __init__(self, resource: AsyncAnalytics) -> None:
self._resource = resource
self.retrieve = async_to_streamed_response_wrapper(resource.retrieve)
self.retrieve_today = async_to_streamed_response_wrapper(resource.retrieve_today)
14 changes: 12 additions & 2 deletions src/didit/resources/bank.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,16 @@ def account_check(
account: BankAccountCheckParamsAccount,
holder: BankAccountCheckHolder,
vendor_data: str | None | NotGiven = NOT_GIVEN,
consent: 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,
) -> BankAccountCheckResponse:
"""Check a bank account.

**Not available in production yet.** Bank Verification is switched off in production, so until it is switched on this endpoint answers `404` with `bank_verification_not_available`. Do not build on it yet. The rest of this description is what the endpoint does once it is available.

Check that a bank account belongs to the person or company you name, before you send money to it. No session and no sign-in: send the name and the account, read the result. It is the [account check](/core-technology/bank-verification/account-check) of the Bank Verification step, run from your backend.

**What you send.** The account's `country` (ISO 3166-1 alpha-3), the `account` the way that country writes it, and the `holder` name: `full_name` for a person or `business_name` for a company. A business name checks a business account.
Expand All @@ -70,7 +73,7 @@ def account_check(

**Every request is a new check.** There is no idempotency key and no endpoint to read a check back, so a retried request runs, and is billed, again. Store the response.

**Sandbox.** A sandbox key calls no bank and is charged nothing. The last 4 characters of the IBAN, else of the account number, choose the answer: `0001` a `close_match` with a `holder_name`, `0002` a `close_match` without one, `0003` and `0006` a `no_match`, `0004` `cannot_verify`, `0005` a `503` and `0009` a `400`. Any other ending is a `match`.
**Sandbox.** Where Bank Verification is available, a sandbox key calls no bank and is charged nothing. The last 4 characters of the IBAN, else of the account number, choose the answer: `0001` a `close_match` with a `holder_name`, `0002` a `close_match` without one, `0003` and `0006` a `no_match`, `0004` `cannot_verify`, `0005` a `503` and `0009` a `400`. Any other ending is a `match`.

**API keys.** A scoped API key needs write access to sessions.

Expand All @@ -81,6 +84,7 @@ def account_check(
account: The account the way its country writes it: an `iban`, or an `account_number` with the bank or branch code the country needs (`sort_code`, `routing_number`, `transit_institution`, `bsb`, `ifsc`, `bank_code` or `bic`). Mexico takes a `clabe` and Argentina a `cbu_cvu`, which already name the bank. A business account adds a `registration_id` where the country asks for one. Spaces, dashes and dots are ignored. Keys the country's check does not use are ignored; a key it needs and does not get is a `400` that names it.
holder: The name the account is checked against. Send exactly one of `full_name` and `business_name`.
vendor_data: Your own reference for this check, returned unchanged.
consent: Set to `true` when the account holder has explicitly consented to this check. Required in the countries whose account check needs that consent: without it the check is refused with a `400` whose `field` is `consent` and whose `reason` is `required`, and nothing is billed. Elsewhere it is optional, and Didit records it with the check's charge when you send 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.
Expand All @@ -98,6 +102,7 @@ def account_check(
"account": account,
"holder": holder,
"vendor_data": vendor_data,
"consent": consent,
},
extra_headers=extra_headers,
extra_query=extra_query,
Expand Down Expand Up @@ -130,13 +135,16 @@ async def account_check(
account: BankAccountCheckParamsAccount,
holder: BankAccountCheckHolder,
vendor_data: str | None | NotGiven = NOT_GIVEN,
consent: 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,
) -> BankAccountCheckResponse:
"""Check a bank account.

**Not available in production yet.** Bank Verification is switched off in production, so until it is switched on this endpoint answers `404` with `bank_verification_not_available`. Do not build on it yet. The rest of this description is what the endpoint does once it is available.

Check that a bank account belongs to the person or company you name, before you send money to it. No session and no sign-in: send the name and the account, read the result. It is the [account check](/core-technology/bank-verification/account-check) of the Bank Verification step, run from your backend.

**What you send.** The account's `country` (ISO 3166-1 alpha-3), the `account` the way that country writes it, and the `holder` name: `full_name` for a person or `business_name` for a company. A business name checks a business account.
Expand All @@ -147,7 +155,7 @@ async def account_check(

**Every request is a new check.** There is no idempotency key and no endpoint to read a check back, so a retried request runs, and is billed, again. Store the response.

**Sandbox.** A sandbox key calls no bank and is charged nothing. The last 4 characters of the IBAN, else of the account number, choose the answer: `0001` a `close_match` with a `holder_name`, `0002` a `close_match` without one, `0003` and `0006` a `no_match`, `0004` `cannot_verify`, `0005` a `503` and `0009` a `400`. Any other ending is a `match`.
**Sandbox.** Where Bank Verification is available, a sandbox key calls no bank and is charged nothing. The last 4 characters of the IBAN, else of the account number, choose the answer: `0001` a `close_match` with a `holder_name`, `0002` a `close_match` without one, `0003` and `0006` a `no_match`, `0004` `cannot_verify`, `0005` a `503` and `0009` a `400`. Any other ending is a `match`.

**API keys.** A scoped API key needs write access to sessions.

Expand All @@ -158,6 +166,7 @@ async def account_check(
account: The account the way its country writes it: an `iban`, or an `account_number` with the bank or branch code the country needs (`sort_code`, `routing_number`, `transit_institution`, `bsb`, `ifsc`, `bank_code` or `bic`). Mexico takes a `clabe` and Argentina a `cbu_cvu`, which already name the bank. A business account adds a `registration_id` where the country asks for one. Spaces, dashes and dots are ignored. Keys the country's check does not use are ignored; a key it needs and does not get is a `400` that names it.
holder: The name the account is checked against. Send exactly one of `full_name` and `business_name`.
vendor_data: Your own reference for this check, returned unchanged.
consent: Set to `true` when the account holder has explicitly consented to this check. Required in the countries whose account check needs that consent: without it the check is refused with a `400` whose `field` is `consent` and whose `reason` is `required`, and nothing is billed. Elsewhere it is optional, and Didit records it with the check's charge when you send 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.
Expand All @@ -175,6 +184,7 @@ async def account_check(
"account": account,
"holder": holder,
"vendor_data": vendor_data,
"consent": consent,
},
extra_headers=extra_headers,
extra_query=extra_query,
Expand Down
Loading
Loading