didit-sdk 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- didit/__init__.py +64 -0
- didit/client.py +204 -0
- didit/config.py +68 -0
- didit/errors.py +83 -0
- didit/integrations/__init__.py +5 -0
- didit/integrations/fastapi.py +72 -0
- didit/models/__init__.py +25 -0
- didit/models/decision.py +85 -0
- didit/models/enums.py +39 -0
- didit/models/session.py +47 -0
- didit/models/webhook.py +30 -0
- didit/py.typed +1 -0
- didit/resources/__init__.py +5 -0
- didit/resources/base.py +85 -0
- didit/resources/sessions.py +150 -0
- didit/simulation.py +320 -0
- didit/webhooks.py +158 -0
- didit_sdk-0.1.0.dist-info/METADATA +262 -0
- didit_sdk-0.1.0.dist-info/RECORD +21 -0
- didit_sdk-0.1.0.dist-info/WHEEL +4 -0
- didit_sdk-0.1.0.dist-info/licenses/LICENSE +28 -0
didit/__init__.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Official-grade Python SDK for Didit Identity Verification & KYC.
|
|
2
|
+
|
|
3
|
+
Disclaimer:
|
|
4
|
+
This is an independent open-source library and is not officially affiliated
|
|
5
|
+
with or endorsed by Didit Protocol Inc.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from didit.client import AsyncDidit, Didit
|
|
9
|
+
from didit.config import DiditConfig
|
|
10
|
+
from didit.errors import (
|
|
11
|
+
DiditAPIError,
|
|
12
|
+
DiditAuthenticationError,
|
|
13
|
+
DiditConfigurationError,
|
|
14
|
+
DiditError,
|
|
15
|
+
DiditNotFoundError,
|
|
16
|
+
DiditRateLimitError,
|
|
17
|
+
DiditServerError,
|
|
18
|
+
DiditSignatureError,
|
|
19
|
+
DiditTimeoutError,
|
|
20
|
+
)
|
|
21
|
+
from didit.models.decision import (
|
|
22
|
+
AMLData,
|
|
23
|
+
BiometricsData,
|
|
24
|
+
DecisionResponse,
|
|
25
|
+
DocumentData,
|
|
26
|
+
ReviewData,
|
|
27
|
+
)
|
|
28
|
+
from didit.models.enums import Language, SessionStatus
|
|
29
|
+
from didit.models.session import CreateSessionRequest, SessionResponse
|
|
30
|
+
from didit.models.webhook import WebhookPayload
|
|
31
|
+
from didit.simulation import SimulatedAsyncDidit, SimulatedDidit
|
|
32
|
+
from didit.webhooks import parse_webhook_payload, verify_webhook_signature
|
|
33
|
+
|
|
34
|
+
__version__ = "0.1.0"
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"AMLData",
|
|
38
|
+
"AsyncDidit",
|
|
39
|
+
"BiometricsData",
|
|
40
|
+
"CreateSessionRequest",
|
|
41
|
+
"DecisionResponse",
|
|
42
|
+
"Didit",
|
|
43
|
+
"DiditAPIError",
|
|
44
|
+
"DiditAuthenticationError",
|
|
45
|
+
"DiditConfig",
|
|
46
|
+
"DiditConfigurationError",
|
|
47
|
+
"DiditError",
|
|
48
|
+
"DiditNotFoundError",
|
|
49
|
+
"DiditRateLimitError",
|
|
50
|
+
"DiditServerError",
|
|
51
|
+
"DiditSignatureError",
|
|
52
|
+
"DiditTimeoutError",
|
|
53
|
+
"DocumentData",
|
|
54
|
+
"Language",
|
|
55
|
+
"ReviewData",
|
|
56
|
+
"SessionResponse",
|
|
57
|
+
"SessionStatus",
|
|
58
|
+
"SimulatedAsyncDidit",
|
|
59
|
+
"SimulatedDidit",
|
|
60
|
+
"WebhookPayload",
|
|
61
|
+
"__version__",
|
|
62
|
+
"parse_webhook_payload",
|
|
63
|
+
"verify_webhook_signature",
|
|
64
|
+
]
|
didit/client.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Synchronous and asynchronous Didit client implementations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from didit.config import DiditConfig
|
|
11
|
+
from didit.errors import DiditConfigurationError
|
|
12
|
+
from didit.models.webhook import WebhookPayload
|
|
13
|
+
from didit.resources.sessions import AsyncSessionsResource, SessionsResource
|
|
14
|
+
from didit.webhooks import parse_webhook_payload, verify_webhook_signature
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Didit:
|
|
18
|
+
"""Synchronous client for the Didit Verification API."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
api_key: str | None = None,
|
|
23
|
+
*,
|
|
24
|
+
base_url: str | None = None,
|
|
25
|
+
timeout: float | None = None,
|
|
26
|
+
max_retries: int | None = None,
|
|
27
|
+
webhook_secret: str | None = None,
|
|
28
|
+
config: DiditConfig | None = None,
|
|
29
|
+
http_client: httpx.Client | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
if config is not None:
|
|
32
|
+
self._config = config
|
|
33
|
+
else:
|
|
34
|
+
self._config = DiditConfig.from_env(
|
|
35
|
+
api_key=api_key,
|
|
36
|
+
base_url=base_url,
|
|
37
|
+
timeout=timeout,
|
|
38
|
+
max_retries=max_retries,
|
|
39
|
+
webhook_secret=webhook_secret,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
self._manage_http = http_client is None
|
|
43
|
+
self._http = http_client or httpx.Client(
|
|
44
|
+
base_url=self._config.base_url,
|
|
45
|
+
headers={
|
|
46
|
+
"x-api-key": self._config.api_key,
|
|
47
|
+
"Accept": "application/json",
|
|
48
|
+
},
|
|
49
|
+
timeout=self._config.timeout,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
self.sessions = SessionsResource(self._http)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def config(self) -> DiditConfig:
|
|
56
|
+
"""Client configuration instance."""
|
|
57
|
+
return self._config
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def http_client(self) -> httpx.Client:
|
|
61
|
+
"""Underlying httpx.Client instance."""
|
|
62
|
+
return self._http
|
|
63
|
+
|
|
64
|
+
def verify_webhook(
|
|
65
|
+
self,
|
|
66
|
+
raw_body: bytes,
|
|
67
|
+
headers: Mapping[str, str],
|
|
68
|
+
*,
|
|
69
|
+
secret: str | None = None,
|
|
70
|
+
max_age_seconds: int | None = None,
|
|
71
|
+
) -> bool:
|
|
72
|
+
"""Verify an incoming webhook's signature and timestamp freshness."""
|
|
73
|
+
wh_secret = secret or self._config.webhook_secret
|
|
74
|
+
if not wh_secret:
|
|
75
|
+
raise DiditConfigurationError(
|
|
76
|
+
"No webhook_secret configured on client. "
|
|
77
|
+
"Provide secret parameter or configure DIDIT_WEBHOOK_SECRET."
|
|
78
|
+
)
|
|
79
|
+
return verify_webhook_signature(
|
|
80
|
+
raw_body, headers, wh_secret, max_age_seconds=max_age_seconds
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def parse_webhook(
|
|
84
|
+
self,
|
|
85
|
+
raw_body: bytes,
|
|
86
|
+
headers: Mapping[str, str],
|
|
87
|
+
*,
|
|
88
|
+
secret: str | None = None,
|
|
89
|
+
max_age_seconds: int | None = None,
|
|
90
|
+
) -> WebhookPayload:
|
|
91
|
+
"""Verify and parse an incoming webhook payload into a WebhookPayload object."""
|
|
92
|
+
wh_secret = secret or self._config.webhook_secret
|
|
93
|
+
if not wh_secret:
|
|
94
|
+
raise DiditConfigurationError(
|
|
95
|
+
"No webhook_secret configured on client. "
|
|
96
|
+
"Provide secret parameter or configure DIDIT_WEBHOOK_SECRET."
|
|
97
|
+
)
|
|
98
|
+
return parse_webhook_payload(raw_body, headers, wh_secret, max_age_seconds=max_age_seconds)
|
|
99
|
+
|
|
100
|
+
def close(self) -> None:
|
|
101
|
+
"""Close the underlying HTTP client."""
|
|
102
|
+
if self._manage_http:
|
|
103
|
+
self._http.close()
|
|
104
|
+
|
|
105
|
+
def __enter__(self) -> Didit:
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
def __exit__(self, *args: Any) -> None:
|
|
109
|
+
self.close()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class AsyncDidit:
|
|
113
|
+
"""Asynchronous client for the Didit Verification API."""
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
api_key: str | None = None,
|
|
118
|
+
*,
|
|
119
|
+
base_url: str | None = None,
|
|
120
|
+
timeout: float | None = None,
|
|
121
|
+
max_retries: int | None = None,
|
|
122
|
+
webhook_secret: str | None = None,
|
|
123
|
+
config: DiditConfig | None = None,
|
|
124
|
+
http_client: httpx.AsyncClient | None = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
if config is not None:
|
|
127
|
+
self._config = config
|
|
128
|
+
else:
|
|
129
|
+
self._config = DiditConfig.from_env(
|
|
130
|
+
api_key=api_key,
|
|
131
|
+
base_url=base_url,
|
|
132
|
+
timeout=timeout,
|
|
133
|
+
max_retries=max_retries,
|
|
134
|
+
webhook_secret=webhook_secret,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
self._manage_http = http_client is None
|
|
138
|
+
self._http = http_client or httpx.AsyncClient(
|
|
139
|
+
base_url=self._config.base_url,
|
|
140
|
+
headers={
|
|
141
|
+
"x-api-key": self._config.api_key,
|
|
142
|
+
"Accept": "application/json",
|
|
143
|
+
},
|
|
144
|
+
timeout=self._config.timeout,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
self.sessions = AsyncSessionsResource(self._http)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def config(self) -> DiditConfig:
|
|
151
|
+
"""Client configuration instance."""
|
|
152
|
+
return self._config
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def http_client(self) -> httpx.AsyncClient:
|
|
156
|
+
"""Underlying httpx.AsyncClient instance."""
|
|
157
|
+
return self._http
|
|
158
|
+
|
|
159
|
+
def verify_webhook(
|
|
160
|
+
self,
|
|
161
|
+
raw_body: bytes,
|
|
162
|
+
headers: Mapping[str, str],
|
|
163
|
+
*,
|
|
164
|
+
secret: str | None = None,
|
|
165
|
+
max_age_seconds: int | None = None,
|
|
166
|
+
) -> bool:
|
|
167
|
+
"""Verify an incoming webhook's signature and timestamp freshness."""
|
|
168
|
+
wh_secret = secret or self._config.webhook_secret
|
|
169
|
+
if not wh_secret:
|
|
170
|
+
raise DiditConfigurationError(
|
|
171
|
+
"No webhook_secret configured on client. "
|
|
172
|
+
"Provide secret parameter or configure DIDIT_WEBHOOK_SECRET."
|
|
173
|
+
)
|
|
174
|
+
return verify_webhook_signature(
|
|
175
|
+
raw_body, headers, wh_secret, max_age_seconds=max_age_seconds
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def parse_webhook(
|
|
179
|
+
self,
|
|
180
|
+
raw_body: bytes,
|
|
181
|
+
headers: Mapping[str, str],
|
|
182
|
+
*,
|
|
183
|
+
secret: str | None = None,
|
|
184
|
+
max_age_seconds: int | None = None,
|
|
185
|
+
) -> WebhookPayload:
|
|
186
|
+
"""Verify and parse an incoming webhook payload into a WebhookPayload object."""
|
|
187
|
+
wh_secret = secret or self._config.webhook_secret
|
|
188
|
+
if not wh_secret:
|
|
189
|
+
raise DiditConfigurationError(
|
|
190
|
+
"No webhook_secret configured on client. "
|
|
191
|
+
"Provide secret parameter or configure DIDIT_WEBHOOK_SECRET."
|
|
192
|
+
)
|
|
193
|
+
return parse_webhook_payload(raw_body, headers, wh_secret, max_age_seconds=max_age_seconds)
|
|
194
|
+
|
|
195
|
+
async def aclose(self) -> None:
|
|
196
|
+
"""Close the underlying asynchronous HTTP client."""
|
|
197
|
+
if self._manage_http:
|
|
198
|
+
await self._http.aclose()
|
|
199
|
+
|
|
200
|
+
async def __aenter__(self) -> AsyncDidit:
|
|
201
|
+
return self
|
|
202
|
+
|
|
203
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
204
|
+
await self.aclose()
|
didit/config.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Configuration and global defaults for Didit SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
DEFAULT_BASE_URL: str = "https://verification.didit.me/v3"
|
|
9
|
+
DEFAULT_TIMEOUT: float = 30.0
|
|
10
|
+
DEFAULT_MAX_RETRIES: int = 2
|
|
11
|
+
DEFAULT_WEBHOOK_MAX_AGE_SECONDS: int = 300 # 5 minutes
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class DiditConfig:
|
|
16
|
+
"""Immutable client configuration."""
|
|
17
|
+
|
|
18
|
+
api_key: str
|
|
19
|
+
base_url: str = DEFAULT_BASE_URL
|
|
20
|
+
timeout: float = DEFAULT_TIMEOUT
|
|
21
|
+
max_retries: int = DEFAULT_MAX_RETRIES
|
|
22
|
+
webhook_secret: str | None = None
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_env(
|
|
26
|
+
cls,
|
|
27
|
+
*,
|
|
28
|
+
api_key: str | None = None,
|
|
29
|
+
base_url: str | None = None,
|
|
30
|
+
timeout: float | None = None,
|
|
31
|
+
max_retries: int | None = None,
|
|
32
|
+
webhook_secret: str | None = None,
|
|
33
|
+
) -> DiditConfig:
|
|
34
|
+
"""Resolve configuration falling back to environment variables."""
|
|
35
|
+
resolved_key = api_key or os.environ.get("DIDIT_API_KEY")
|
|
36
|
+
if not resolved_key:
|
|
37
|
+
from didit.errors import DiditConfigurationError
|
|
38
|
+
|
|
39
|
+
raise DiditConfigurationError(
|
|
40
|
+
"Missing Didit API key. "
|
|
41
|
+
"Provide api_key or set the DIDIT_API_KEY environment variable."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
resolved_base_url = (
|
|
45
|
+
base_url or os.environ.get("DIDIT_BASE_URL") or DEFAULT_BASE_URL
|
|
46
|
+
).rstrip("/")
|
|
47
|
+
|
|
48
|
+
resolved_timeout = (
|
|
49
|
+
timeout
|
|
50
|
+
if timeout is not None
|
|
51
|
+
else float(os.environ.get("DIDIT_TIMEOUT", DEFAULT_TIMEOUT))
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
resolved_retries = (
|
|
55
|
+
max_retries
|
|
56
|
+
if max_retries is not None
|
|
57
|
+
else int(os.environ.get("DIDIT_MAX_RETRIES", DEFAULT_MAX_RETRIES))
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
resolved_secret = webhook_secret or os.environ.get("DIDIT_WEBHOOK_SECRET")
|
|
61
|
+
|
|
62
|
+
return cls(
|
|
63
|
+
api_key=resolved_key,
|
|
64
|
+
base_url=resolved_base_url,
|
|
65
|
+
timeout=resolved_timeout,
|
|
66
|
+
max_retries=resolved_retries,
|
|
67
|
+
webhook_secret=resolved_secret,
|
|
68
|
+
)
|
didit/errors.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Exception hierarchy for didit-sdk."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DiditError(Exception):
|
|
10
|
+
"""Base exception for all Didit SDK errors."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DiditConfigurationError(DiditError):
|
|
14
|
+
"""Raised when the client is configured with missing or invalid parameters."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DiditSignatureError(DiditError):
|
|
18
|
+
"""Raised when a webhook signature fails cryptographic verification or freshness check."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DiditTimeoutError(DiditError):
|
|
22
|
+
"""Raised when an operation such as polling exceeds the configured timeout limit."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DiditAPIError(DiditError):
|
|
26
|
+
"""Raised when the Didit API returns an HTTP error status code."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
message: str,
|
|
31
|
+
*,
|
|
32
|
+
status_code: int,
|
|
33
|
+
response_body: str | None = None,
|
|
34
|
+
headers: Mapping[str, str] | None = None,
|
|
35
|
+
error_code: str | None = None,
|
|
36
|
+
details: Any = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
super().__init__(message)
|
|
39
|
+
self.status_code = status_code
|
|
40
|
+
self.response_body = response_body
|
|
41
|
+
self.headers = dict(headers) if headers else {}
|
|
42
|
+
self.error_code = error_code
|
|
43
|
+
self.details = details
|
|
44
|
+
|
|
45
|
+
def __repr__(self) -> str:
|
|
46
|
+
return (
|
|
47
|
+
f"{self.__class__.__name__}(status_code={self.status_code}, "
|
|
48
|
+
f"error_code={self.error_code!r}, message={str(self)!r})"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DiditAuthenticationError(DiditAPIError):
|
|
53
|
+
"""Raised on 401 Unauthorized or 403 Forbidden responses (invalid API key)."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DiditNotFoundError(DiditAPIError):
|
|
57
|
+
"""Raised on 404 Not Found responses (session or resource does not exist)."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class DiditRateLimitError(DiditAPIError):
|
|
61
|
+
"""Raised on 429 Too Many Requests responses."""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
message: str,
|
|
66
|
+
*,
|
|
67
|
+
status_code: int = 429,
|
|
68
|
+
response_body: str | None = None,
|
|
69
|
+
headers: Mapping[str, str] | None = None,
|
|
70
|
+
retry_after: float | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
super().__init__(
|
|
73
|
+
message,
|
|
74
|
+
status_code=status_code,
|
|
75
|
+
response_body=response_body,
|
|
76
|
+
headers=headers,
|
|
77
|
+
error_code="RATE_LIMIT_EXCEEDED",
|
|
78
|
+
)
|
|
79
|
+
self.retry_after = retry_after
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class DiditServerError(DiditAPIError):
|
|
83
|
+
"""Raised on 5xx Internal Server Error responses."""
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""FastAPI integration utilities and webhook security guard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
from starlette.requests import Request
|
|
9
|
+
|
|
10
|
+
from didit.config import DEFAULT_WEBHOOK_MAX_AGE_SECONDS
|
|
11
|
+
from didit.errors import DiditConfigurationError
|
|
12
|
+
from didit.models.webhook import WebhookPayload
|
|
13
|
+
from didit.webhooks import verify_webhook_signature
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DiditWebhookGuard:
|
|
17
|
+
"""FastAPI dependency for verifying and parsing Didit webhook requests.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
```python
|
|
21
|
+
guard = DiditWebhookGuard(secret="whsec_...")
|
|
22
|
+
|
|
23
|
+
@app.post("/webhooks/didit")
|
|
24
|
+
async def handle_webhook(payload: WebhookPayload = Depends(guard)):
|
|
25
|
+
if payload.status == SessionStatus.APPROVED:
|
|
26
|
+
# Process approved KYC verification
|
|
27
|
+
...
|
|
28
|
+
```
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
secret: str | None = None,
|
|
34
|
+
*,
|
|
35
|
+
max_age_seconds: int = DEFAULT_WEBHOOK_MAX_AGE_SECONDS,
|
|
36
|
+
) -> None:
|
|
37
|
+
resolved_secret = secret or os.environ.get("DIDIT_WEBHOOK_SECRET")
|
|
38
|
+
if not resolved_secret:
|
|
39
|
+
raise DiditConfigurationError(
|
|
40
|
+
"Missing webhook secret. "
|
|
41
|
+
"Provide secret parameter or set DIDIT_WEBHOOK_SECRET environment variable."
|
|
42
|
+
)
|
|
43
|
+
self.secret: str = resolved_secret
|
|
44
|
+
self.max_age_seconds = max_age_seconds
|
|
45
|
+
|
|
46
|
+
async def __call__(self, request: Request) -> WebhookPayload:
|
|
47
|
+
from fastapi import HTTPException
|
|
48
|
+
|
|
49
|
+
raw_body = await request.body()
|
|
50
|
+
try:
|
|
51
|
+
body_dict = json.loads(raw_body.decode("utf-8"))
|
|
52
|
+
except (ValueError, UnicodeDecodeError):
|
|
53
|
+
raise HTTPException(status_code=400, detail="Malformed JSON in webhook body") from None
|
|
54
|
+
|
|
55
|
+
if not isinstance(body_dict, dict):
|
|
56
|
+
raise HTTPException(status_code=400, detail="Malformed JSON in webhook body")
|
|
57
|
+
|
|
58
|
+
is_valid = verify_webhook_signature(
|
|
59
|
+
raw_body,
|
|
60
|
+
request.headers,
|
|
61
|
+
self.secret,
|
|
62
|
+
max_age_seconds=self.max_age_seconds,
|
|
63
|
+
)
|
|
64
|
+
if not is_valid:
|
|
65
|
+
raise HTTPException(
|
|
66
|
+
status_code=401,
|
|
67
|
+
detail="Invalid webhook signature or expired timestamp",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
payload = WebhookPayload.model_validate(body_dict)
|
|
71
|
+
payload.raw_data = body_dict
|
|
72
|
+
return payload
|
didit/models/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Domain and data models for Didit SDK."""
|
|
2
|
+
|
|
3
|
+
from didit.models.decision import (
|
|
4
|
+
AMLData,
|
|
5
|
+
BiometricsData,
|
|
6
|
+
DecisionResponse,
|
|
7
|
+
DocumentData,
|
|
8
|
+
ReviewData,
|
|
9
|
+
)
|
|
10
|
+
from didit.models.enums import Language, SessionStatus
|
|
11
|
+
from didit.models.session import CreateSessionRequest, SessionResponse
|
|
12
|
+
from didit.models.webhook import WebhookPayload
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AMLData",
|
|
16
|
+
"BiometricsData",
|
|
17
|
+
"CreateSessionRequest",
|
|
18
|
+
"DecisionResponse",
|
|
19
|
+
"DocumentData",
|
|
20
|
+
"Language",
|
|
21
|
+
"ReviewData",
|
|
22
|
+
"SessionResponse",
|
|
23
|
+
"SessionStatus",
|
|
24
|
+
"WebhookPayload",
|
|
25
|
+
]
|
didit/models/decision.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Decision and verification result data models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
from didit.models.enums import SessionStatus
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DocumentData(BaseModel):
|
|
13
|
+
"""Document extraction and validation data."""
|
|
14
|
+
|
|
15
|
+
model_config = ConfigDict(extra="ignore")
|
|
16
|
+
|
|
17
|
+
document_type: str | None = Field(
|
|
18
|
+
default=None, description="e.g. passport, id_card, drivers_license"
|
|
19
|
+
)
|
|
20
|
+
country: str | None = Field(default=None, description="ISO-3 country code")
|
|
21
|
+
document_number: str | None = Field(default=None, description="Masked or raw document number")
|
|
22
|
+
first_name: str | None = Field(default=None, description="Given names")
|
|
23
|
+
last_name: str | None = Field(default=None, description="Family names")
|
|
24
|
+
date_of_birth: str | None = Field(default=None, description="YYYY-MM-DD format")
|
|
25
|
+
expiration_date: str | None = Field(default=None, description="YYYY-MM-DD format")
|
|
26
|
+
is_valid: bool | None = Field(
|
|
27
|
+
default=None, description="Whether document authenticity checks passed"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BiometricsData(BaseModel):
|
|
32
|
+
"""Facial matching and liveness assessment."""
|
|
33
|
+
|
|
34
|
+
model_config = ConfigDict(extra="ignore")
|
|
35
|
+
|
|
36
|
+
face_match: bool | None = Field(default=None, description="Face match against document photo")
|
|
37
|
+
liveness_check: bool | None = Field(
|
|
38
|
+
default=None, description="Active or passive liveness check"
|
|
39
|
+
)
|
|
40
|
+
score: float | None = Field(
|
|
41
|
+
default=None, description="Biometric confidence score between 0.0 and 1.0"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AMLData(BaseModel):
|
|
46
|
+
"""Anti-Money Laundering and Watchlist screening results."""
|
|
47
|
+
|
|
48
|
+
model_config = ConfigDict(extra="ignore")
|
|
49
|
+
|
|
50
|
+
pep_detected: bool | None = Field(
|
|
51
|
+
default=None, description="Politically Exposed Person detection"
|
|
52
|
+
)
|
|
53
|
+
sanctions_detected: bool | None = Field(
|
|
54
|
+
default=None, description="International sanctions list match"
|
|
55
|
+
)
|
|
56
|
+
adverse_media_detected: bool | None = Field(default=None, description="Adverse media match")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ReviewData(BaseModel):
|
|
60
|
+
"""Manual or compliance agent review information."""
|
|
61
|
+
|
|
62
|
+
model_config = ConfigDict(extra="ignore")
|
|
63
|
+
|
|
64
|
+
reviewed_by: str | None = Field(default=None, description="Identifier of the reviewer")
|
|
65
|
+
decision_reason: str | None = Field(default=None, description="Reviewer explanation or notes")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class DecisionResponse(BaseModel):
|
|
69
|
+
"""Complete verification decision returned by Didit."""
|
|
70
|
+
|
|
71
|
+
model_config = ConfigDict(extra="ignore")
|
|
72
|
+
|
|
73
|
+
session_id: str = Field(..., description="Unique session identifier")
|
|
74
|
+
status: SessionStatus = Field(..., description="Final or current verification status")
|
|
75
|
+
workflow_id: str | None = Field(default=None, description="Associated workflow identifier")
|
|
76
|
+
vendor_data: str | None = Field(default=None, description="Echoed vendor reference")
|
|
77
|
+
document: DocumentData | None = Field(default=None, description="Document verification details")
|
|
78
|
+
biometrics: BiometricsData | None = Field(
|
|
79
|
+
default=None, description="Biometric matching details"
|
|
80
|
+
)
|
|
81
|
+
aml: AMLData | None = Field(default=None, description="AML / Watchlist screening details")
|
|
82
|
+
review: ReviewData | None = Field(default=None, description="Human review details")
|
|
83
|
+
raw_data: dict[str, Any] | None = Field(
|
|
84
|
+
default=None, description="Raw JSON payload received from Didit"
|
|
85
|
+
)
|
didit/models/enums.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Enumerations for Didit verification statuses and configurations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SessionStatus(str, Enum):
|
|
9
|
+
"""Case-sensitive session statuses defined by Didit's Sessions API."""
|
|
10
|
+
|
|
11
|
+
NOT_STARTED = "Not Started"
|
|
12
|
+
IN_PROGRESS = "In Progress"
|
|
13
|
+
IN_REVIEW = "In Review"
|
|
14
|
+
APPROVED = "Approved"
|
|
15
|
+
DECLINED = "Declined"
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def is_terminal(self) -> bool:
|
|
19
|
+
"""Return True if the verification workflow has finished (Approved or Declined)."""
|
|
20
|
+
return self in (SessionStatus.APPROVED, SessionStatus.DECLINED)
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def is_in_review(self) -> bool:
|
|
24
|
+
"""Return True if the verification requires manual human review."""
|
|
25
|
+
return self == SessionStatus.IN_REVIEW
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Language(str, Enum):
|
|
29
|
+
"""Supported UI languages for Didit hosted verification pages."""
|
|
30
|
+
|
|
31
|
+
EN = "en"
|
|
32
|
+
ES = "es"
|
|
33
|
+
FR = "fr"
|
|
34
|
+
DE = "de"
|
|
35
|
+
IT = "it"
|
|
36
|
+
PT = "pt"
|
|
37
|
+
CA = "ca"
|
|
38
|
+
EU = "eu"
|
|
39
|
+
GL = "gl"
|