ontiver 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.
ontiver/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ from .client import OntiverClient
2
+ from .errors import (
3
+ AuthenticationError,
4
+ NetworkError,
5
+ NotFoundError,
6
+ OntiverAPIError,
7
+ OntiverConfigError,
8
+ OntiverError,
9
+ PermissionDeniedError,
10
+ RateLimitError,
11
+ ServerError,
12
+ TimeoutError,
13
+ ValidationError,
14
+ WebhookSignatureError,
15
+ )
16
+
17
+ __version__ = "0.1.0"
18
+
19
+ __all__ = [
20
+ "OntiverClient",
21
+ "OntiverError",
22
+ "OntiverConfigError",
23
+ "OntiverAPIError",
24
+ "AuthenticationError",
25
+ "PermissionDeniedError",
26
+ "ValidationError",
27
+ "RateLimitError",
28
+ "NotFoundError",
29
+ "ServerError",
30
+ "TimeoutError",
31
+ "NetworkError",
32
+ "WebhookSignatureError",
33
+ ]
ontiver/client.py ADDED
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+ import httpx
7
+
8
+ from .config import OntiverConfig
9
+ from .http import OntiverHTTPClient
10
+ from .resources.proofs import ProofsResource
11
+ from .resources.screening import ScreeningResource
12
+ from .resources.verifications import VerificationsResource
13
+ from .resources.webhook_management import WebhookManagementResource
14
+
15
+
16
+ class OntiverClient:
17
+ def __init__(
18
+ self,
19
+ api_key: str | None = None,
20
+ base_url: str | None = None,
21
+ environment: str | None = None,
22
+ timeout: float = 30,
23
+ max_retries: int = 2,
24
+ webhook_secret: str | None = None,
25
+ http_client: httpx.Client | None = None,
26
+ ) -> None:
27
+ self.config = OntiverConfig.from_env(
28
+ api_key=api_key,
29
+ base_url=base_url,
30
+ environment=environment,
31
+ webhook_secret=webhook_secret,
32
+ timeout=timeout,
33
+ max_retries=max_retries,
34
+ )
35
+ self._http = OntiverHTTPClient(self.config, client=http_client)
36
+ self.webhooks = WebhookManagementResource(self._http)
37
+ self.verifications = VerificationsResource(self._http)
38
+ self.screening = ScreeningResource(self._http)
39
+ self.proofs = ProofsResource(self._http)
40
+
41
+ @classmethod
42
+ def from_env(cls) -> "OntiverClient":
43
+ return cls()
44
+
45
+ def close(self) -> None:
46
+ self._http.close()
47
+
48
+ def __enter__(self) -> "OntiverClient":
49
+ return self
50
+
51
+ def __exit__(self, *_: object) -> None:
52
+ self.close()
53
+
54
+ def validate_api_key(self) -> dict[str, Any]:
55
+ return self._http.request("GET", "/api-keys/validate/current")
56
+
57
+ def raw_request(
58
+ self,
59
+ method: str,
60
+ path: str,
61
+ *,
62
+ json: Any | None = None,
63
+ params: Mapping[str, Any] | None = None,
64
+ headers: Mapping[str, str] | None = None,
65
+ idempotency_key: str | None = None,
66
+ ) -> Any:
67
+ return self._http.request(
68
+ method,
69
+ path,
70
+ json=json,
71
+ params=params,
72
+ headers=headers,
73
+ idempotency_key=idempotency_key,
74
+ unwrap=False,
75
+ )
ontiver/config.py ADDED
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from urllib.parse import urlparse
6
+
7
+ from .errors import OntiverConfigError
8
+
9
+
10
+ DEFAULT_BASE_URL = "https://api.ontiver.com"
11
+ VALID_ENVIRONMENTS = {"production", "sandbox"}
12
+ LOCAL_HOSTS = {"localhost", "127.0.0.1", "0.0.0.0", "::1"}
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class OntiverConfig:
17
+ api_key: str | None = None
18
+ base_url: str = DEFAULT_BASE_URL
19
+ environment: str = "production"
20
+ webhook_secret: str | None = None
21
+ timeout: float = 30
22
+ max_retries: int = 2
23
+
24
+ @classmethod
25
+ def from_env(
26
+ cls,
27
+ *,
28
+ api_key: str | None = None,
29
+ base_url: str | None = None,
30
+ environment: str | None = None,
31
+ webhook_secret: str | None = None,
32
+ timeout: float = 30,
33
+ max_retries: int = 2,
34
+ ) -> "OntiverConfig":
35
+ return cls(
36
+ api_key=api_key if api_key is not None else os.getenv("ONTIVER_API_KEY"),
37
+ base_url=base_url or os.getenv("ONTIVER_BASE_URL") or DEFAULT_BASE_URL,
38
+ environment=environment or os.getenv("ONTIVER_ENVIRONMENT") or "production",
39
+ webhook_secret=webhook_secret if webhook_secret is not None else os.getenv("ONTIVER_WEBHOOK_SECRET"),
40
+ timeout=timeout,
41
+ max_retries=max_retries,
42
+ ).validated()
43
+
44
+ def validated(self) -> "OntiverConfig":
45
+ environment = str(self.environment or "").strip().lower()
46
+ if environment not in VALID_ENVIRONMENTS:
47
+ raise OntiverConfigError("environment must be 'production' or 'sandbox'")
48
+ base_url = normalize_base_url(self.base_url)
49
+ return OntiverConfig(
50
+ api_key=str(self.api_key).strip() if self.api_key else None,
51
+ base_url=base_url,
52
+ environment=environment,
53
+ webhook_secret=str(self.webhook_secret).strip() if self.webhook_secret else None,
54
+ timeout=float(self.timeout),
55
+ max_retries=max(0, int(self.max_retries)),
56
+ )
57
+
58
+
59
+ def normalize_base_url(value: str | None) -> str:
60
+ raw = str(value or DEFAULT_BASE_URL).strip().rstrip("/")
61
+ parsed = urlparse(raw)
62
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
63
+ raise OntiverConfigError("base_url must be an absolute HTTP(S) URL")
64
+ hostname = (parsed.hostname or "").lower()
65
+ if parsed.scheme != "https" and hostname not in LOCAL_HOSTS:
66
+ raise OntiverConfigError("HTTPS is required for non-local Ontiver API base URLs")
67
+ return raw
ontiver/errors.py ADDED
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any
5
+
6
+
7
+ _SENSITIVE_PATTERNS = [
8
+ re.compile(r"Bearer\s+[A-Za-z0-9._\-]+", re.IGNORECASE),
9
+ re.compile(r"\b(prod|sb)_[A-Za-z0-9._\-]+"),
10
+ re.compile(r"\bwhsec_[A-Za-z0-9._\-]+"),
11
+ re.compile(r"\bsec_[A-Za-z0-9._\-]+"),
12
+ ]
13
+
14
+
15
+ def redact(value: Any) -> str:
16
+ text = str(value or "")
17
+ for pattern in _SENSITIVE_PATTERNS:
18
+ text = pattern.sub("[REDACTED]", text)
19
+ return text
20
+
21
+
22
+ class OntiverError(Exception):
23
+ """Base SDK exception."""
24
+
25
+
26
+ class OntiverConfigError(OntiverError):
27
+ """Raised when SDK configuration is missing or unsafe."""
28
+
29
+
30
+ class OntiverAPIError(OntiverError):
31
+ def __init__(
32
+ self,
33
+ message: str,
34
+ *,
35
+ status_code: int | None = None,
36
+ code: str | None = None,
37
+ request_id: str | None = None,
38
+ ) -> None:
39
+ self.status_code = status_code
40
+ self.code = code
41
+ self.request_id = request_id
42
+ super().__init__(redact(message))
43
+
44
+
45
+ class AuthenticationError(OntiverAPIError):
46
+ pass
47
+
48
+
49
+ class PermissionDeniedError(OntiverAPIError):
50
+ pass
51
+
52
+
53
+ class ValidationError(OntiverAPIError):
54
+ pass
55
+
56
+
57
+ class RateLimitError(OntiverAPIError):
58
+ pass
59
+
60
+
61
+ class NotFoundError(OntiverAPIError):
62
+ pass
63
+
64
+
65
+ class ServerError(OntiverAPIError):
66
+ pass
67
+
68
+
69
+ class TimeoutError(OntiverAPIError):
70
+ pass
71
+
72
+
73
+ class NetworkError(OntiverAPIError):
74
+ pass
75
+
76
+
77
+ class WebhookSignatureError(OntiverError):
78
+ pass
ontiver/http.py ADDED
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ import uuid
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ from .config import OntiverConfig
11
+ from .errors import (
12
+ AuthenticationError,
13
+ NetworkError,
14
+ NotFoundError,
15
+ OntiverAPIError,
16
+ OntiverConfigError,
17
+ PermissionDeniedError,
18
+ RateLimitError,
19
+ ServerError,
20
+ TimeoutError,
21
+ ValidationError,
22
+ redact,
23
+ )
24
+
25
+
26
+ RETRYABLE_STATUS_CODES = {408, 429, 500, 502, 503, 504}
27
+ MUTATING_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
28
+ USER_AGENT_VERSION = "0.1.0"
29
+
30
+
31
+ class OntiverHTTPClient:
32
+ def __init__(self, config: OntiverConfig, *, client: httpx.Client | None = None) -> None:
33
+ self.config = config
34
+ self._client = client or httpx.Client(timeout=config.timeout)
35
+
36
+ def close(self) -> None:
37
+ self._client.close()
38
+
39
+ def request(
40
+ self,
41
+ method: str,
42
+ path: str,
43
+ *,
44
+ json: Any | None = None,
45
+ params: Mapping[str, Any] | None = None,
46
+ headers: Mapping[str, str] | None = None,
47
+ idempotency_key: str | None = None,
48
+ unwrap: bool = True,
49
+ authenticated: bool = True,
50
+ auto_idempotency: bool = False,
51
+ ) -> Any:
52
+ method = method.upper()
53
+ url = self._url(path)
54
+ request_headers = self._headers(headers, authenticated=authenticated)
55
+ if method in MUTATING_METHODS and auto_idempotency and not idempotency_key:
56
+ idempotency_key = f"ontiver-sdk-{uuid.uuid4()}"
57
+ if idempotency_key:
58
+ request_headers["Idempotency-Key"] = idempotency_key
59
+
60
+ can_retry_mutation = method not in MUTATING_METHODS or bool(idempotency_key)
61
+ attempts = self.config.max_retries + 1 if can_retry_mutation else 1
62
+ last_exc: Exception | None = None
63
+ for attempt in range(attempts):
64
+ try:
65
+ response = self._client.request(method, url, json=json, params=params, headers=request_headers)
66
+ except httpx.TimeoutException as exc:
67
+ last_exc = exc
68
+ if attempt + 1 >= attempts:
69
+ raise TimeoutError("Ontiver request timed out") from exc
70
+ self._sleep(attempt, None)
71
+ continue
72
+ except httpx.HTTPError as exc:
73
+ last_exc = exc
74
+ if attempt + 1 >= attempts:
75
+ raise NetworkError("Ontiver network request failed") from exc
76
+ self._sleep(attempt, None)
77
+ continue
78
+
79
+ if response.status_code in RETRYABLE_STATUS_CODES and attempt + 1 < attempts:
80
+ self._sleep(attempt, response)
81
+ continue
82
+ return self._parse_response(response, unwrap=unwrap)
83
+
84
+ raise NetworkError(f"Ontiver request failed: {redact(last_exc)}")
85
+
86
+ def _url(self, path: str) -> str:
87
+ if not path.startswith("/"):
88
+ raise ValidationError("API path must start with '/'")
89
+ normalized = path if path.startswith("/api/") else f"/api/v1/banking{path}"
90
+ return f"{self.config.base_url}{normalized}"
91
+
92
+ def _headers(self, headers: Mapping[str, str] | None, *, authenticated: bool) -> dict[str, str]:
93
+ out = {
94
+ "Accept": "application/json",
95
+ "Content-Type": "application/json",
96
+ "User-Agent": f"ontiver-python/{USER_AGENT_VERSION}",
97
+ }
98
+ if headers:
99
+ for key, value in headers.items():
100
+ if key.lower() == "authorization":
101
+ continue
102
+ out[key] = value
103
+ if authenticated:
104
+ if not self.config.api_key:
105
+ raise OntiverConfigError("ONTIVER_API_KEY is required for authenticated requests")
106
+ out["Authorization"] = f"Bearer {self.config.api_key}"
107
+ return out
108
+
109
+ def _parse_response(self, response: httpx.Response, *, unwrap: bool) -> Any:
110
+ request_id = response.headers.get("x-request-id") or response.headers.get("X-Request-Id")
111
+ try:
112
+ body = response.json()
113
+ except ValueError:
114
+ body = {"message": response.text}
115
+ if response.status_code >= 400:
116
+ raise self._error_for_response(response.status_code, body, request_id)
117
+ if unwrap and isinstance(body, dict) and "success" in body and "data" in body:
118
+ return body["data"]
119
+ return body
120
+
121
+ def _error_for_response(self, status_code: int, body: Any, request_id: str | None) -> OntiverAPIError:
122
+ code, message = self._extract_error(body)
123
+ kwargs = {"status_code": status_code, "code": code, "request_id": request_id}
124
+ if status_code == 401:
125
+ return AuthenticationError(message, **kwargs)
126
+ if status_code == 403:
127
+ return PermissionDeniedError(message, **kwargs)
128
+ if status_code == 404:
129
+ return NotFoundError(message, **kwargs)
130
+ if status_code in {400, 409, 422}:
131
+ return ValidationError(message, **kwargs)
132
+ if status_code == 408:
133
+ return TimeoutError(message, **kwargs)
134
+ if status_code == 429:
135
+ return RateLimitError(message, **kwargs)
136
+ if status_code >= 500:
137
+ return ServerError(message, **kwargs)
138
+ return OntiverAPIError(message, **kwargs)
139
+
140
+ def _extract_error(self, body: Any) -> tuple[str | None, str]:
141
+ if not isinstance(body, dict):
142
+ return None, "Ontiver API request failed"
143
+ detail = body.get("detail") or body.get("error") or body.get("message")
144
+ if isinstance(detail, dict):
145
+ code = detail.get("code")
146
+ message = detail.get("message") or detail.get("detail") or "Ontiver API request failed"
147
+ return str(code) if code else None, redact(message)
148
+ if isinstance(detail, str):
149
+ return str(body.get("code")) if body.get("code") else None, redact(detail)
150
+ return str(body.get("code")) if body.get("code") else None, "Ontiver API request failed"
151
+
152
+ def _sleep(self, attempt: int, response: httpx.Response | None) -> None:
153
+ retry_after = response.headers.get("Retry-After") if response is not None else None
154
+ if retry_after:
155
+ try:
156
+ delay = min(float(retry_after), 5.0)
157
+ except ValueError:
158
+ delay = 0.25
159
+ else:
160
+ delay = min(0.25 * (2**attempt), 2.0)
161
+ time.sleep(delay)
ontiver/py.typed ADDED
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..http import OntiverHTTPClient
6
+
7
+
8
+ class ProofsResource:
9
+ def __init__(self, http: OntiverHTTPClient) -> None:
10
+ self._http = http
11
+
12
+ def generate(
13
+ self,
14
+ proof_type: str,
15
+ statement: str,
16
+ *,
17
+ verification_id: str | None = None,
18
+ witness: dict[str, Any] | None = None,
19
+ public_signals: dict[str, Any] | None = None,
20
+ disclosure_fields: list[str] | None = None,
21
+ idempotency_key: str | None = None,
22
+ ) -> dict[str, Any]:
23
+ return self._http.request(
24
+ "POST",
25
+ "/zk-proof/generate",
26
+ json={
27
+ "proofType": proof_type,
28
+ "statement": statement,
29
+ "verificationId": verification_id,
30
+ "witness": witness,
31
+ "publicSignals": public_signals,
32
+ "disclosureFields": disclosure_fields,
33
+ },
34
+ idempotency_key=idempotency_key,
35
+ auto_idempotency=True,
36
+ )
37
+
38
+ def get_for_verification(self, verification_id: str) -> dict[str, Any]:
39
+ return self._http.request("GET", f"/zk-proof/verification/{verification_id}")
40
+
41
+ def verify(self, proof_id: str, *, proof: dict[str, Any] | None = None, idempotency_key: str | None = None) -> dict[str, Any]:
42
+ return self._http.request(
43
+ "POST",
44
+ "/zk-proof/verify",
45
+ json={"proofId": proof_id, "proof": proof},
46
+ idempotency_key=idempotency_key,
47
+ auto_idempotency=True,
48
+ )
49
+
50
+ def disclose(self, proof_id: str, *, fields: list[str] | None = None, idempotency_key: str | None = None) -> dict[str, Any]:
51
+ return self._http.request(
52
+ "POST",
53
+ "/zk-proof/disclose",
54
+ json={"proofId": proof_id, "fields": fields or []},
55
+ idempotency_key=idempotency_key,
56
+ auto_idempotency=True,
57
+ )
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..http import OntiverHTTPClient
6
+
7
+
8
+ class ScreeningResource:
9
+ def __init__(self, http: OntiverHTTPClient) -> None:
10
+ self._http = http
11
+
12
+ def pep_check(
13
+ self,
14
+ *,
15
+ name: str | None = None,
16
+ first_name: str | None = None,
17
+ last_name: str | None = None,
18
+ date_of_birth: str | None = None,
19
+ nationality: str | None = None,
20
+ fuzzy_matching: bool = True,
21
+ match_threshold: int = 90,
22
+ idempotency_key: str | None = None,
23
+ ) -> dict[str, Any]:
24
+ return self._check("/screening/pep/check", name, first_name, last_name, date_of_birth, nationality, fuzzy_matching, match_threshold, idempotency_key)
25
+
26
+ def sanctions_check(
27
+ self,
28
+ *,
29
+ name: str | None = None,
30
+ first_name: str | None = None,
31
+ last_name: str | None = None,
32
+ date_of_birth: str | None = None,
33
+ nationality: str | None = None,
34
+ fuzzy_matching: bool = True,
35
+ match_threshold: int = 90,
36
+ idempotency_key: str | None = None,
37
+ ) -> dict[str, Any]:
38
+ return self._check("/screening/sanctions/check", name, first_name, last_name, date_of_birth, nationality, fuzzy_matching, match_threshold, idempotency_key)
39
+
40
+ def adverse_media_check(
41
+ self,
42
+ first_name: str,
43
+ last_name: str,
44
+ *,
45
+ fuzzy_matching: bool = True,
46
+ match_threshold: int = 90,
47
+ idempotency_key: str | None = None,
48
+ ) -> dict[str, Any]:
49
+ return self._http.request(
50
+ "POST",
51
+ "/screening/adverse-media/check",
52
+ json={"firstName": first_name, "lastName": last_name, "fuzzyMatching": fuzzy_matching, "matchThreshold": match_threshold},
53
+ idempotency_key=idempotency_key,
54
+ auto_idempotency=True,
55
+ )
56
+
57
+ def _check(
58
+ self,
59
+ path: str,
60
+ name: str | None,
61
+ first_name: str | None,
62
+ last_name: str | None,
63
+ date_of_birth: str | None,
64
+ nationality: str | None,
65
+ fuzzy_matching: bool,
66
+ match_threshold: int,
67
+ idempotency_key: str | None,
68
+ ) -> dict[str, Any]:
69
+ payload = {
70
+ "name": name,
71
+ "firstName": first_name,
72
+ "lastName": last_name,
73
+ "dateOfBirth": date_of_birth,
74
+ "nationality": nationality,
75
+ "fuzzyMatching": fuzzy_matching,
76
+ "matchThreshold": match_threshold,
77
+ }
78
+ return self._http.request("POST", path, json=payload, idempotency_key=idempotency_key, auto_idempotency=True)
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..http import OntiverHTTPClient
6
+
7
+
8
+ class VerificationsResource:
9
+ def __init__(self, http: OntiverHTTPClient) -> None:
10
+ self._http = http
11
+
12
+ def create(self, payload: dict[str, Any], *, idempotency_key: str | None = None) -> dict[str, Any]:
13
+ return self._http.request(
14
+ "POST",
15
+ "/kyc/individual/verify",
16
+ json=payload,
17
+ idempotency_key=idempotency_key,
18
+ auto_idempotency=True,
19
+ )
20
+
21
+ def get(self, verification_id: str) -> dict[str, Any]:
22
+ return self._http.request("GET", f"/kyc/individual/{verification_id}")
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..http import OntiverHTTPClient
6
+
7
+
8
+ class WebhookManagementResource:
9
+ def __init__(self, http: OntiverHTTPClient) -> None:
10
+ self._http = http
11
+
12
+ def register(
13
+ self,
14
+ url: str,
15
+ events: list[str],
16
+ *,
17
+ secret: str | None = None,
18
+ active: bool = True,
19
+ idempotency_key: str | None = None,
20
+ ) -> dict[str, Any]:
21
+ return self._http.request(
22
+ "POST",
23
+ "/webhooks/register",
24
+ json={"url": url, "events": events, "secret": secret, "active": active},
25
+ idempotency_key=idempotency_key,
26
+ auto_idempotency=True,
27
+ )
28
+
29
+ def list(self, *, active: bool | None = None) -> dict[str, Any]:
30
+ params = {} if active is None else {"active": active}
31
+ return self._http.request("GET", "/webhooks", params=params)
32
+
33
+ def delete(self, webhook_id: str, *, idempotency_key: str | None = None) -> dict[str, Any]:
34
+ return self._http.request(
35
+ "DELETE",
36
+ f"/webhooks/{webhook_id}",
37
+ idempotency_key=idempotency_key,
38
+ auto_idempotency=True,
39
+ )
40
+
41
+ def test(
42
+ self,
43
+ *,
44
+ webhook_id: str | None = None,
45
+ url: str | None = None,
46
+ event_type: str = "verification.completed",
47
+ payload: dict[str, Any] | None = None,
48
+ idempotency_key: str | None = None,
49
+ ) -> dict[str, Any]:
50
+ body = {"webhookId": webhook_id, "webhookUrl": url, "eventType": event_type, "payload": payload or {}}
51
+ path = f"/webhooks/{webhook_id}/test" if webhook_id else "/webhooks/test"
52
+ return self._http.request("POST", path, json=body, idempotency_key=idempotency_key, auto_idempotency=True)
53
+
54
+ def rotate_secret(self, webhook_id: str, *, idempotency_key: str | None = None) -> dict[str, Any]:
55
+ return self._http.request(
56
+ "POST",
57
+ f"/webhooks/{webhook_id}/rotate-secret",
58
+ idempotency_key=idempotency_key,
59
+ auto_idempotency=True,
60
+ )
ontiver/webhooks.py ADDED
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ import hmac
4
+ from hashlib import sha256
5
+
6
+ from .errors import WebhookSignatureError
7
+
8
+
9
+ def verify_webhook_signature(payload: bytes | str, signature: str | None, secret: str) -> bool:
10
+ if not signature or not secret:
11
+ return False
12
+ raw = payload.encode("utf-8") if isinstance(payload, str) else payload
13
+ provided = signature.strip()
14
+ if provided.startswith("sha256="):
15
+ provided = provided[len("sha256=") :]
16
+ digest = hmac.new(secret.encode("utf-8"), raw, sha256).hexdigest()
17
+ return hmac.compare_digest(digest, provided)
18
+
19
+
20
+ def require_webhook_signature(payload: bytes | str, signature: str | None, secret: str) -> None:
21
+ if not verify_webhook_signature(payload, signature, secret):
22
+ raise WebhookSignatureError("Invalid Ontiver webhook signature")
@@ -0,0 +1,238 @@
1
+ Metadata-Version: 2.4
2
+ Name: ontiver
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the Ontiver API
5
+ Author: Ontiver
6
+ License-Expression: LicenseRef-Proprietary
7
+ Project-URL: Homepage, https://ontiver.com
8
+ Project-URL: Documentation, https://docs.ontiver.com
9
+ Project-URL: Repository, https://github.com/joinverza/Ontiver
10
+ Keywords: ontiver,kyc,aml,identity,proofs,webhooks
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: httpx<1,>=0.27
20
+ Provides-Extra: test
21
+ Requires-Dist: pytest>=8; extra == "test"
22
+ Provides-Extra: build
23
+ Requires-Dist: build>=1.2; extra == "build"
24
+ Requires-Dist: twine>=5; extra == "build"
25
+
26
+ # Ontiver Python SDK
27
+
28
+ Official Python SDK for integrating Ontiver verification, screening, proofs, and webhooks into enterprise backends.
29
+
30
+ The dashboard remains the control plane for API keys, scopes, billing, team roles, webhooks, and audit logs. The SDK is the integration layer for Django, Flask, FastAPI, workers, and internal services.
31
+
32
+ ## Install Locally
33
+
34
+ From this repository:
35
+
36
+ ```bash
37
+ cd sdks/python
38
+ python -m pip install -e ".[test]"
39
+ ```
40
+
41
+ After publication:
42
+
43
+ ```bash
44
+ python -m pip install ontiver
45
+ ```
46
+
47
+ ## Configuration
48
+
49
+ Create an API key from the Ontiver enterprise dashboard and store it in your secret manager.
50
+
51
+ ```bash
52
+ ONTIVER_API_KEY=prod_xxx
53
+ ONTIVER_BASE_URL=https://api.ontiver.com
54
+ ONTIVER_ENVIRONMENT=production
55
+ ONTIVER_WEBHOOK_SECRET=whsec_xxx
56
+ ```
57
+
58
+ Use sandbox keys in non-production environments:
59
+
60
+ ```bash
61
+ ONTIVER_API_KEY=sb_xxx
62
+ ONTIVER_ENVIRONMENT=sandbox
63
+ ```
64
+
65
+ ## Quickstart
66
+
67
+ ```python
68
+ from ontiver import OntiverClient
69
+
70
+ client = OntiverClient.from_env()
71
+
72
+ startup_check = client.validate_api_key()
73
+ print(startup_check["valid"])
74
+ ```
75
+
76
+ ## Verification
77
+
78
+ SDK v1 routes flexible verification payloads to:
79
+
80
+ ```text
81
+ POST /api/v1/banking/kyc/individual/verify
82
+ GET /api/v1/banking/kyc/individual/{verificationId}
83
+ ```
84
+
85
+ ```python
86
+ result = client.verifications.create(
87
+ {
88
+ "customerId": "cust_123",
89
+ "firstName": "Ada",
90
+ "lastName": "Okafor",
91
+ "country": "NG",
92
+ "idDocumentType": "passport",
93
+ "idDocumentNumber": "A12345678",
94
+ },
95
+ idempotency_key="verify-cust-123",
96
+ )
97
+
98
+ status = client.verifications.get(result["verificationId"])
99
+ ```
100
+
101
+ ## Screening
102
+
103
+ ```python
104
+ screening = client.screening.sanctions_check(
105
+ first_name="Ada",
106
+ last_name="Okafor",
107
+ nationality="NG",
108
+ idempotency_key="sanctions-cust-123",
109
+ )
110
+ ```
111
+
112
+ Supported methods:
113
+
114
+ - `client.screening.pep_check(...)`
115
+ - `client.screening.sanctions_check(...)`
116
+ - `client.screening.adverse_media_check(first_name, last_name, ...)`
117
+
118
+ ## Proofs
119
+
120
+ ```python
121
+ proof = client.proofs.get_for_verification("ver_123")
122
+
123
+ verified = client.proofs.verify(
124
+ proof_id=proof["proofId"],
125
+ proof=proof.get("proof"),
126
+ )
127
+ ```
128
+
129
+ Supported methods:
130
+
131
+ - `client.proofs.generate(...)`
132
+ - `client.proofs.get_for_verification(verification_id)`
133
+ - `client.proofs.verify(proof_id, proof=None)`
134
+ - `client.proofs.disclose(proof_id, fields=[...])`
135
+
136
+ ## Webhooks
137
+
138
+ Register a webhook:
139
+
140
+ ```python
141
+ webhook = client.webhooks.register(
142
+ "https://example.com/webhooks/ontiver",
143
+ ["verification.completed", "proof.issued"],
144
+ )
145
+
146
+ # Store this once. It will not be returned by list endpoints.
147
+ secret = webhook["secret"]
148
+ ```
149
+
150
+ Verify webhook signatures using the raw request body:
151
+
152
+ ```python
153
+ from ontiver.webhooks import verify_webhook_signature
154
+
155
+ is_valid = verify_webhook_signature(raw_body, request.headers["X-Ontiver-Signature"], secret)
156
+ ```
157
+
158
+ Header format:
159
+
160
+ ```text
161
+ X-Ontiver-Signature: sha256=<hex_digest>
162
+ ```
163
+
164
+ ## Raw Request Escape Hatch
165
+
166
+ Use `raw_request()` for routes that do not have first-class wrappers yet:
167
+
168
+ ```python
169
+ result = client.raw_request(
170
+ "POST",
171
+ "/aml/smile-checks",
172
+ json={"fullName": "Ada Okafor", "countries": ["NG"]},
173
+ idempotency_key="aml-cust-123",
174
+ )
175
+ ```
176
+
177
+ ## Errors
178
+
179
+ Catch specific exceptions:
180
+
181
+ ```python
182
+ from ontiver import AuthenticationError, PermissionDeniedError, RateLimitError, ValidationError
183
+
184
+ try:
185
+ client.validate_api_key()
186
+ except AuthenticationError:
187
+ ...
188
+ except PermissionDeniedError:
189
+ ...
190
+ except RateLimitError:
191
+ ...
192
+ except ValidationError:
193
+ ...
194
+ ```
195
+
196
+ The SDK redacts API keys, bearer tokens, webhook secrets, and secret prefixes from exception messages.
197
+
198
+ ## Security Notes
199
+
200
+ - Never commit API keys or webhook secrets.
201
+ - Store production secrets in a secret manager.
202
+ - Use least-privilege API key scopes.
203
+ - Use sandbox keys outside production.
204
+ - Verify webhook signatures over the raw body bytes before parsing JSON.
205
+ - Do not log raw identity documents, ID numbers, biometric payloads, or full provider payloads.
206
+
207
+ ## Build And Publish
208
+
209
+ Build locally:
210
+
211
+ ```bash
212
+ cd sdks/python
213
+ python -m pip install -e ".[test]"
214
+ pytest
215
+ python -m pip install ".[build]"
216
+ python -m build
217
+ python -m twine check dist/*
218
+ ```
219
+
220
+ Publish first to TestPyPI:
221
+
222
+ ```bash
223
+ python -m twine upload --repository testpypi dist/*
224
+ ```
225
+
226
+ Install from TestPyPI in a clean environment:
227
+
228
+ ```bash
229
+ python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple ontiver
230
+ ```
231
+
232
+ Publish to PyPI after TestPyPI install passes:
233
+
234
+ ```bash
235
+ python -m twine upload dist/*
236
+ ```
237
+
238
+ Target package name: `ontiver`.
@@ -0,0 +1,16 @@
1
+ ontiver/__init__.py,sha256=27ivsYmiNZDcUcEgjJ0ii4Z_iopJg73swm_jOC3_Dco,651
2
+ ontiver/client.py,sha256=lP_39wundvdjRS5xHXHG3n7_nnuw_bxggNwnLmDQ8fo,2201
3
+ ontiver/config.py,sha256=RcxzeBEtb4DPvKbeTSF2uWte2azoK0kX_1FsNRTfq38,2500
4
+ ontiver/errors.py,sha256=7GuKHLwMTG2T7iFxxzmgK9vd6jPsGtCOUuNPRU2Hz40,1459
5
+ ontiver/http.py,sha256=F9DrwXSv-4YwUiE1FeFmIHCQVHK5UBD82HWM2AIGx7U,6375
6
+ ontiver/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
+ ontiver/webhooks.py,sha256=kxE7xcnqCh3gBFFlKrRKPm2Hs56OcZy8YySQW9d0w4U,823
8
+ ontiver/resources/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
9
+ ontiver/resources/proofs.py,sha256=evPvliiFueeBBuO3dGpOWvDLpD0obWw2krEcrbgtrqw,1949
10
+ ontiver/resources/screening.py,sha256=k-efMsRZh28A2sbzFiT9SN0gljFbSuLViLUyrbtOFvI,2611
11
+ ontiver/resources/verifications.py,sha256=vHv02UTKNzGRt-wpftOnsMia8aOJuoTTJeHw3BKbLrQ,664
12
+ ontiver/resources/webhook_management.py,sha256=fwyR8dnc6a4_Wn0-9ouvjXCsoDsY3p8nq0uSZJi3O9Y,2074
13
+ ontiver-0.1.0.dist-info/METADATA,sha256=5j-2s9i5ZyUP-SGhFBjutcGc0dpOKI1oTa1czCnB2zM,5669
14
+ ontiver-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
15
+ ontiver-0.1.0.dist-info/top_level.txt,sha256=umC5Q1gRMQ-rSI3UCMlFG-c-jrJ_T0J47iLkC7hEHUE,8
16
+ ontiver-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ ontiver