lorica-sdk 0.1.0__tar.gz
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.
- lorica_sdk-0.1.0/LICENSE +21 -0
- lorica_sdk-0.1.0/PKG-INFO +133 -0
- lorica_sdk-0.1.0/README.md +106 -0
- lorica_sdk-0.1.0/lorica/__init__.py +6 -0
- lorica_sdk-0.1.0/lorica/client.py +335 -0
- lorica_sdk-0.1.0/lorica_sdk.egg-info/PKG-INFO +133 -0
- lorica_sdk-0.1.0/lorica_sdk.egg-info/SOURCES.txt +10 -0
- lorica_sdk-0.1.0/lorica_sdk.egg-info/dependency_links.txt +1 -0
- lorica_sdk-0.1.0/lorica_sdk.egg-info/top_level.txt +1 -0
- lorica_sdk-0.1.0/pyproject.toml +37 -0
- lorica_sdk-0.1.0/setup.cfg +4 -0
- lorica_sdk-0.1.0/tests/test_client.py +25 -0
lorica_sdk-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lorica Labs, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lorica-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Lorica biometric verification API
|
|
5
|
+
Author-email: Tristan Linardos <tristan@loricaapi.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://loricaapi.com
|
|
8
|
+
Project-URL: Documentation, https://loricaapi.com/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/LeoMirren/lorica
|
|
10
|
+
Project-URL: Demo, https://loricaapi.com/demo
|
|
11
|
+
Keywords: biometric,verification,identity,liveness,jwt,api
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# lorica
|
|
29
|
+
|
|
30
|
+
Python SDK for the [Lorica](https://loricaapi.com) biometric verification API.
|
|
31
|
+
|
|
32
|
+
Prove a human did it. One API call before any high-risk action. 16 security layers. Under 2 seconds. $0.05.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install lorica
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from lorica import LoricaClient
|
|
44
|
+
|
|
45
|
+
client = LoricaClient("lrca_live_...")
|
|
46
|
+
|
|
47
|
+
# Enroll a user (once)
|
|
48
|
+
client.enroll(user_id="usr_123", image=face_base64)
|
|
49
|
+
|
|
50
|
+
# Verify before any high-risk action
|
|
51
|
+
result = client.verify(
|
|
52
|
+
user_id="usr_123",
|
|
53
|
+
image=face_base64,
|
|
54
|
+
action_context="wire_transfer_50k",
|
|
55
|
+
liveness_mode="passive" # or "dual_frame" or "motion"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if result["match"]:
|
|
59
|
+
jwt = result["token"] # signed proof of authorization
|
|
60
|
+
process_transfer(jwt)
|
|
61
|
+
else:
|
|
62
|
+
block_action(result["rejection_reason"])
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Read Image from File
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
image_b64 = LoricaClient.image_from_file("photo.jpg")
|
|
69
|
+
client.verify(user_id="usr_123", image=image_b64, action_context="trade")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Multi-Party Verification
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
result = client.verify_multi(
|
|
76
|
+
verifications=[
|
|
77
|
+
{"user_id": "trader_001", "image": trader_face, "role": "trader"},
|
|
78
|
+
{"user_id": "risk_mgr", "image": manager_face, "role": "risk_manager"},
|
|
79
|
+
],
|
|
80
|
+
action_context="otc_block_trade",
|
|
81
|
+
require_all=True
|
|
82
|
+
)
|
|
83
|
+
# result["all_verified"] == True, result["token"] = single JWT
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Pre-Action Identity Lock
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
lock = client.create_lock(
|
|
90
|
+
user_id="usr_123",
|
|
91
|
+
image=face_base64,
|
|
92
|
+
action_context="multi_venue_execution",
|
|
93
|
+
duration_seconds=120
|
|
94
|
+
)
|
|
95
|
+
# lock["session_id"], lock["expires_at"]
|
|
96
|
+
|
|
97
|
+
client.lock_heartbeat(lock["session_id"]) # extend
|
|
98
|
+
client.lock_revoke(lock["session_id"]) # revoke
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Analytics
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
client.stats(period="30d")
|
|
105
|
+
client.user_report(user_id="usr_123")
|
|
106
|
+
client.billing_usage()
|
|
107
|
+
client.thresholds() # AI-recommended thresholds
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Error Handling
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from lorica import LoricaClient
|
|
114
|
+
from lorica.client import LoricaError
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
result = client.verify(user_id="usr_123", image=face)
|
|
118
|
+
except LoricaError as e:
|
|
119
|
+
print(e.status_code) # 404
|
|
120
|
+
print(e.error_code) # "user_not_enrolled"
|
|
121
|
+
print(e.message) # "No enrollment found..."
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Links
|
|
125
|
+
|
|
126
|
+
- [Documentation](https://loricaapi.com/docs)
|
|
127
|
+
- [Live Demo](https://loricaapi.com/demo)
|
|
128
|
+
- [Security](https://loricaapi.com/security)
|
|
129
|
+
- [Blog](https://loricaapi.com/blog/)
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# lorica
|
|
2
|
+
|
|
3
|
+
Python SDK for the [Lorica](https://loricaapi.com) biometric verification API.
|
|
4
|
+
|
|
5
|
+
Prove a human did it. One API call before any high-risk action. 16 security layers. Under 2 seconds. $0.05.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install lorica
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from lorica import LoricaClient
|
|
17
|
+
|
|
18
|
+
client = LoricaClient("lrca_live_...")
|
|
19
|
+
|
|
20
|
+
# Enroll a user (once)
|
|
21
|
+
client.enroll(user_id="usr_123", image=face_base64)
|
|
22
|
+
|
|
23
|
+
# Verify before any high-risk action
|
|
24
|
+
result = client.verify(
|
|
25
|
+
user_id="usr_123",
|
|
26
|
+
image=face_base64,
|
|
27
|
+
action_context="wire_transfer_50k",
|
|
28
|
+
liveness_mode="passive" # or "dual_frame" or "motion"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if result["match"]:
|
|
32
|
+
jwt = result["token"] # signed proof of authorization
|
|
33
|
+
process_transfer(jwt)
|
|
34
|
+
else:
|
|
35
|
+
block_action(result["rejection_reason"])
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Read Image from File
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
image_b64 = LoricaClient.image_from_file("photo.jpg")
|
|
42
|
+
client.verify(user_id="usr_123", image=image_b64, action_context="trade")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Multi-Party Verification
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
result = client.verify_multi(
|
|
49
|
+
verifications=[
|
|
50
|
+
{"user_id": "trader_001", "image": trader_face, "role": "trader"},
|
|
51
|
+
{"user_id": "risk_mgr", "image": manager_face, "role": "risk_manager"},
|
|
52
|
+
],
|
|
53
|
+
action_context="otc_block_trade",
|
|
54
|
+
require_all=True
|
|
55
|
+
)
|
|
56
|
+
# result["all_verified"] == True, result["token"] = single JWT
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Pre-Action Identity Lock
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
lock = client.create_lock(
|
|
63
|
+
user_id="usr_123",
|
|
64
|
+
image=face_base64,
|
|
65
|
+
action_context="multi_venue_execution",
|
|
66
|
+
duration_seconds=120
|
|
67
|
+
)
|
|
68
|
+
# lock["session_id"], lock["expires_at"]
|
|
69
|
+
|
|
70
|
+
client.lock_heartbeat(lock["session_id"]) # extend
|
|
71
|
+
client.lock_revoke(lock["session_id"]) # revoke
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Analytics
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
client.stats(period="30d")
|
|
78
|
+
client.user_report(user_id="usr_123")
|
|
79
|
+
client.billing_usage()
|
|
80
|
+
client.thresholds() # AI-recommended thresholds
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Error Handling
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from lorica import LoricaClient
|
|
87
|
+
from lorica.client import LoricaError
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
result = client.verify(user_id="usr_123", image=face)
|
|
91
|
+
except LoricaError as e:
|
|
92
|
+
print(e.status_code) # 404
|
|
93
|
+
print(e.error_code) # "user_not_enrolled"
|
|
94
|
+
print(e.message) # "No enrollment found..."
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Links
|
|
98
|
+
|
|
99
|
+
- [Documentation](https://loricaapi.com/docs)
|
|
100
|
+
- [Live Demo](https://loricaapi.com/demo)
|
|
101
|
+
- [Security](https://loricaapi.com/security)
|
|
102
|
+
- [Blog](https://loricaapi.com/blog/)
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""Lorica Python SDK client."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import base64
|
|
5
|
+
import urllib.request
|
|
6
|
+
import urllib.error
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LoricaError(Exception):
|
|
11
|
+
"""Raised when the Lorica API returns an error."""
|
|
12
|
+
def __init__(self, status_code: int, error_code: str, message: str):
|
|
13
|
+
self.status_code = status_code
|
|
14
|
+
self.error_code = error_code
|
|
15
|
+
self.message = message
|
|
16
|
+
super().__init__(f"[{status_code}] {error_code}: {message}")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LoricaClient:
|
|
20
|
+
"""
|
|
21
|
+
Client for the Lorica biometric verification API.
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
from lorica import LoricaClient
|
|
25
|
+
|
|
26
|
+
client = LoricaClient("lrca_live_...")
|
|
27
|
+
|
|
28
|
+
# Enroll a user
|
|
29
|
+
result = client.enroll(user_id="usr_123", image=base64_face)
|
|
30
|
+
|
|
31
|
+
# Verify before a high-risk action
|
|
32
|
+
result = client.verify(
|
|
33
|
+
user_id="usr_123",
|
|
34
|
+
image=base64_face,
|
|
35
|
+
action_context="wire_transfer_50k"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if result["match"]:
|
|
39
|
+
jwt = result["token"]
|
|
40
|
+
# proceed with action
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
DEFAULT_URL = "https://lorica-production.up.railway.app"
|
|
44
|
+
|
|
45
|
+
def __init__(self, api_key: str, base_url: Optional[str] = None, timeout: int = 30):
|
|
46
|
+
"""
|
|
47
|
+
Initialize the Lorica client.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
api_key: Your Lorica API key (starts with lrca_ or lorica-)
|
|
51
|
+
base_url: API base URL (default: production)
|
|
52
|
+
timeout: Request timeout in seconds (default: 30)
|
|
53
|
+
"""
|
|
54
|
+
self.api_key = api_key
|
|
55
|
+
self.base_url = (base_url or self.DEFAULT_URL).rstrip("/")
|
|
56
|
+
self.timeout = timeout
|
|
57
|
+
|
|
58
|
+
def _request(self, method: str, path: str, body: dict = None) -> dict:
|
|
59
|
+
"""Make an HTTP request to the Lorica API."""
|
|
60
|
+
url = f"{self.base_url}{path}"
|
|
61
|
+
headers = {
|
|
62
|
+
"X-API-Key": self.api_key,
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
}
|
|
65
|
+
data = json.dumps(body).encode() if body else None
|
|
66
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
67
|
+
try:
|
|
68
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
69
|
+
return json.loads(resp.read().decode())
|
|
70
|
+
except urllib.error.HTTPError as e:
|
|
71
|
+
try:
|
|
72
|
+
err_body = json.loads(e.read().decode())
|
|
73
|
+
error_code = err_body.get("error_code", "") or ""
|
|
74
|
+
if isinstance(err_body.get("detail"), dict):
|
|
75
|
+
error_code = err_body["detail"].get("error_code", error_code)
|
|
76
|
+
message = err_body["detail"].get("error_message", str(err_body))
|
|
77
|
+
else:
|
|
78
|
+
message = err_body.get("error_message", str(err_body))
|
|
79
|
+
except Exception:
|
|
80
|
+
error_code = "unknown"
|
|
81
|
+
message = str(e)
|
|
82
|
+
raise LoricaError(e.code, error_code, message)
|
|
83
|
+
except urllib.error.URLError as e:
|
|
84
|
+
raise LoricaError(0, "connection_error", str(e.reason))
|
|
85
|
+
|
|
86
|
+
# ── Core ──────────────────────────────────
|
|
87
|
+
|
|
88
|
+
def health(self) -> dict:
|
|
89
|
+
"""Check API health status."""
|
|
90
|
+
return self._request("GET", "/health")
|
|
91
|
+
|
|
92
|
+
def enroll(self, user_id: str, image: str, images: list = None) -> dict:
|
|
93
|
+
"""
|
|
94
|
+
Enroll a user with a face image.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
user_id: Unique user identifier
|
|
98
|
+
image: Base64-encoded face image (for single enrollment)
|
|
99
|
+
images: List of base64-encoded images (for multi-image enrollment, 2-5)
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
{"success": True, "user_id": "..."}
|
|
103
|
+
"""
|
|
104
|
+
body = {"user_id": user_id}
|
|
105
|
+
if images:
|
|
106
|
+
body["images"] = images
|
|
107
|
+
else:
|
|
108
|
+
body["image"] = image
|
|
109
|
+
return self._request("POST", "/enroll", body)
|
|
110
|
+
|
|
111
|
+
def verify(
|
|
112
|
+
self,
|
|
113
|
+
user_id: str,
|
|
114
|
+
image: str,
|
|
115
|
+
action_context: str = "high_risk_action",
|
|
116
|
+
liveness_mode: str = "passive",
|
|
117
|
+
policy_id: str = None,
|
|
118
|
+
token_ttl_seconds: int = None,
|
|
119
|
+
) -> dict:
|
|
120
|
+
"""
|
|
121
|
+
Verify a user before a high-risk action.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
user_id: The enrolled user to verify
|
|
125
|
+
image: Base64-encoded face image
|
|
126
|
+
action_context: What action is being authorized (e.g. "wire_transfer_50k")
|
|
127
|
+
liveness_mode: "passive", "dual_frame", or "motion"
|
|
128
|
+
policy_id: Optional policy to apply
|
|
129
|
+
token_ttl_seconds: Optional JWT TTL override (30-86400)
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
{
|
|
133
|
+
"match": True/False,
|
|
134
|
+
"confidence": 0.94,
|
|
135
|
+
"confidence_level": "high",
|
|
136
|
+
"liveness_score": 0.91,
|
|
137
|
+
"token": "eyJ...",
|
|
138
|
+
"verification_id": "...",
|
|
139
|
+
...
|
|
140
|
+
}
|
|
141
|
+
"""
|
|
142
|
+
body = {
|
|
143
|
+
"user_id": user_id,
|
|
144
|
+
"image": image,
|
|
145
|
+
"action_context": action_context,
|
|
146
|
+
"liveness_mode": liveness_mode,
|
|
147
|
+
}
|
|
148
|
+
if policy_id:
|
|
149
|
+
body["policy_id"] = policy_id
|
|
150
|
+
if token_ttl_seconds:
|
|
151
|
+
body["token_ttl_seconds"] = token_ttl_seconds
|
|
152
|
+
return self._request("POST", "/verify", body)
|
|
153
|
+
|
|
154
|
+
def verify_multi(
|
|
155
|
+
self,
|
|
156
|
+
verifications: list,
|
|
157
|
+
action_context: str = "high_risk_action",
|
|
158
|
+
require_all: bool = True,
|
|
159
|
+
) -> dict:
|
|
160
|
+
"""
|
|
161
|
+
Multi-party verification. Two or more people verify together.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
verifications: List of {"user_id": "...", "image": "...", "role": "..."}
|
|
165
|
+
action_context: What action is being authorized
|
|
166
|
+
require_all: Whether all parties must pass (default: True)
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
{"all_verified": True, "token": "eyJ...", "parties": [...]}
|
|
170
|
+
"""
|
|
171
|
+
return self._request("POST", "/verify-multi", {
|
|
172
|
+
"verifications": verifications,
|
|
173
|
+
"action_context": action_context,
|
|
174
|
+
"require_all": require_all,
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
def verify_batch(self, verifications: list) -> dict:
|
|
178
|
+
"""
|
|
179
|
+
Batch verification. Process 1-50 verifications at once.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
verifications: List of {"user_id": "...", "image": "...", "action_context": "..."}
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
{"batch_id": "...", "total": 5, "passed": 4, "failed": 1, "results": [...], "token": "eyJ..."}
|
|
186
|
+
"""
|
|
187
|
+
return self._request("POST", "/verify/batch", {"verifications": verifications})
|
|
188
|
+
|
|
189
|
+
# ── Locks ─────────────────────────────────
|
|
190
|
+
|
|
191
|
+
def create_lock(
|
|
192
|
+
self,
|
|
193
|
+
user_id: str,
|
|
194
|
+
image: str,
|
|
195
|
+
action_context: str,
|
|
196
|
+
duration_seconds: int = 60,
|
|
197
|
+
) -> dict:
|
|
198
|
+
"""
|
|
199
|
+
Create a pre-action identity lock.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
user_id: The enrolled user
|
|
203
|
+
image: Base64-encoded face image
|
|
204
|
+
action_context: What action the lock covers
|
|
205
|
+
duration_seconds: How long the lock lasts (default: 60)
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
{"session_id": "...", "status": "active", "expires_at": "..."}
|
|
209
|
+
"""
|
|
210
|
+
return self._request("POST", "/lock", {
|
|
211
|
+
"user_id": user_id,
|
|
212
|
+
"image": image,
|
|
213
|
+
"action_context": action_context,
|
|
214
|
+
"duration_seconds": duration_seconds,
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
def lock_status(self, session_id: str) -> dict:
|
|
218
|
+
"""Check lock status."""
|
|
219
|
+
return self._request("GET", f"/lock/{session_id}")
|
|
220
|
+
|
|
221
|
+
def lock_heartbeat(self, session_id: str) -> dict:
|
|
222
|
+
"""Extend a lock."""
|
|
223
|
+
return self._request("POST", f"/lock/{session_id}/heartbeat")
|
|
224
|
+
|
|
225
|
+
def lock_revoke(self, session_id: str, reason: str = None) -> dict:
|
|
226
|
+
"""Revoke a lock."""
|
|
227
|
+
body = {}
|
|
228
|
+
if reason:
|
|
229
|
+
body["reason"] = reason
|
|
230
|
+
return self._request("POST", f"/lock/{session_id}/revoke", body or None)
|
|
231
|
+
|
|
232
|
+
# ── Chains ────────────────────────────────
|
|
233
|
+
|
|
234
|
+
def create_chain(self, action_context: str = None, max_verifications: int = 50) -> dict:
|
|
235
|
+
"""Create a verification chain."""
|
|
236
|
+
body = {"max_verifications": max_verifications}
|
|
237
|
+
if action_context:
|
|
238
|
+
body["action_context"] = action_context
|
|
239
|
+
return self._request("POST", "/chains", body)
|
|
240
|
+
|
|
241
|
+
def get_chain(self, chain_id: str) -> dict:
|
|
242
|
+
"""Get chain status."""
|
|
243
|
+
return self._request("GET", f"/chains/{chain_id}")
|
|
244
|
+
|
|
245
|
+
def close_chain(self, chain_id: str) -> dict:
|
|
246
|
+
"""Close a chain and generate chain JWT."""
|
|
247
|
+
return self._request("POST", f"/chains/{chain_id}/close")
|
|
248
|
+
|
|
249
|
+
def chain_jwt(self, chain_id: str) -> dict:
|
|
250
|
+
"""Get the JWT for a closed chain."""
|
|
251
|
+
return self._request("GET", f"/chains/{chain_id}/jwt")
|
|
252
|
+
|
|
253
|
+
# ── Analytics ─────────────────────────────
|
|
254
|
+
|
|
255
|
+
def stats(self, period: str = "7d") -> dict:
|
|
256
|
+
"""Get aggregate verification stats."""
|
|
257
|
+
return self._request("GET", f"/stats?period={period}")
|
|
258
|
+
|
|
259
|
+
def verifications(self, limit: int = 50, user_id: str = None) -> dict:
|
|
260
|
+
"""Get verification history."""
|
|
261
|
+
path = f"/verifications?limit={limit}"
|
|
262
|
+
if user_id:
|
|
263
|
+
path += f"&user_id={user_id}"
|
|
264
|
+
return self._request("GET", path)
|
|
265
|
+
|
|
266
|
+
def user_report(self, user_id: str) -> dict:
|
|
267
|
+
"""Get comprehensive user verification report."""
|
|
268
|
+
return self._request("GET", f"/users/{user_id}/report")
|
|
269
|
+
|
|
270
|
+
def billing_usage(self, period: str = None) -> dict:
|
|
271
|
+
"""Get billing usage for current or specified period."""
|
|
272
|
+
path = "/billing/usage"
|
|
273
|
+
if period:
|
|
274
|
+
path += f"?period={period}"
|
|
275
|
+
return self._request("GET", path)
|
|
276
|
+
|
|
277
|
+
def thresholds(self) -> dict:
|
|
278
|
+
"""Get AI-recommended verification thresholds."""
|
|
279
|
+
return self._request("GET", "/thresholds/recommend")
|
|
280
|
+
|
|
281
|
+
# ── Policies ──────────────────────────────
|
|
282
|
+
|
|
283
|
+
def create_policy(self, policy_id: str, **kwargs) -> dict:
|
|
284
|
+
"""Create a verification policy."""
|
|
285
|
+
body = {"policy_id": policy_id, **kwargs}
|
|
286
|
+
return self._request("POST", "/policies", body)
|
|
287
|
+
|
|
288
|
+
def get_policy(self, policy_id: str) -> dict:
|
|
289
|
+
"""Get a policy."""
|
|
290
|
+
return self._request("GET", f"/policies/{policy_id}")
|
|
291
|
+
|
|
292
|
+
def update_policy(self, policy_id: str, **kwargs) -> dict:
|
|
293
|
+
"""Update a policy."""
|
|
294
|
+
return self._request("PUT", f"/policies/{policy_id}", kwargs)
|
|
295
|
+
|
|
296
|
+
def delete_policy(self, policy_id: str) -> dict:
|
|
297
|
+
"""Delete a policy."""
|
|
298
|
+
return self._request("DELETE", f"/policies/{policy_id}")
|
|
299
|
+
|
|
300
|
+
# ── Actions ───────────────────────────────
|
|
301
|
+
|
|
302
|
+
def create_action(self, action_id: str, **kwargs) -> dict:
|
|
303
|
+
"""Register a custom action."""
|
|
304
|
+
body = {"action_id": action_id, **kwargs}
|
|
305
|
+
return self._request("POST", "/actions", body)
|
|
306
|
+
|
|
307
|
+
def list_actions(self) -> dict:
|
|
308
|
+
"""List all registered actions."""
|
|
309
|
+
return self._request("GET", "/actions")
|
|
310
|
+
|
|
311
|
+
def delete_action(self, action_id: str) -> dict:
|
|
312
|
+
"""Delete a custom action."""
|
|
313
|
+
return self._request("DELETE", f"/actions/{action_id}")
|
|
314
|
+
|
|
315
|
+
# ── Webhooks ──────────────────────────────
|
|
316
|
+
|
|
317
|
+
def create_webhook(self, url: str, events: list) -> dict:
|
|
318
|
+
"""Register a webhook."""
|
|
319
|
+
return self._request("POST", "/webhooks", {"url": url, "events": events})
|
|
320
|
+
|
|
321
|
+
def list_webhooks(self) -> dict:
|
|
322
|
+
"""List all webhooks."""
|
|
323
|
+
return self._request("GET", "/webhooks")
|
|
324
|
+
|
|
325
|
+
def delete_webhook(self, webhook_id: str) -> dict:
|
|
326
|
+
"""Delete a webhook."""
|
|
327
|
+
return self._request("DELETE", f"/webhooks/{webhook_id}")
|
|
328
|
+
|
|
329
|
+
# ── Helpers ───────────────────────────────
|
|
330
|
+
|
|
331
|
+
@staticmethod
|
|
332
|
+
def image_from_file(path: str) -> str:
|
|
333
|
+
"""Read an image file and return base64-encoded string."""
|
|
334
|
+
with open(path, "rb") as f:
|
|
335
|
+
return base64.b64encode(f.read()).decode()
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lorica-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Lorica biometric verification API
|
|
5
|
+
Author-email: Tristan Linardos <tristan@loricaapi.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://loricaapi.com
|
|
8
|
+
Project-URL: Documentation, https://loricaapi.com/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/LeoMirren/lorica
|
|
10
|
+
Project-URL: Demo, https://loricaapi.com/demo
|
|
11
|
+
Keywords: biometric,verification,identity,liveness,jwt,api
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# lorica
|
|
29
|
+
|
|
30
|
+
Python SDK for the [Lorica](https://loricaapi.com) biometric verification API.
|
|
31
|
+
|
|
32
|
+
Prove a human did it. One API call before any high-risk action. 16 security layers. Under 2 seconds. $0.05.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install lorica
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from lorica import LoricaClient
|
|
44
|
+
|
|
45
|
+
client = LoricaClient("lrca_live_...")
|
|
46
|
+
|
|
47
|
+
# Enroll a user (once)
|
|
48
|
+
client.enroll(user_id="usr_123", image=face_base64)
|
|
49
|
+
|
|
50
|
+
# Verify before any high-risk action
|
|
51
|
+
result = client.verify(
|
|
52
|
+
user_id="usr_123",
|
|
53
|
+
image=face_base64,
|
|
54
|
+
action_context="wire_transfer_50k",
|
|
55
|
+
liveness_mode="passive" # or "dual_frame" or "motion"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if result["match"]:
|
|
59
|
+
jwt = result["token"] # signed proof of authorization
|
|
60
|
+
process_transfer(jwt)
|
|
61
|
+
else:
|
|
62
|
+
block_action(result["rejection_reason"])
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Read Image from File
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
image_b64 = LoricaClient.image_from_file("photo.jpg")
|
|
69
|
+
client.verify(user_id="usr_123", image=image_b64, action_context="trade")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Multi-Party Verification
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
result = client.verify_multi(
|
|
76
|
+
verifications=[
|
|
77
|
+
{"user_id": "trader_001", "image": trader_face, "role": "trader"},
|
|
78
|
+
{"user_id": "risk_mgr", "image": manager_face, "role": "risk_manager"},
|
|
79
|
+
],
|
|
80
|
+
action_context="otc_block_trade",
|
|
81
|
+
require_all=True
|
|
82
|
+
)
|
|
83
|
+
# result["all_verified"] == True, result["token"] = single JWT
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Pre-Action Identity Lock
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
lock = client.create_lock(
|
|
90
|
+
user_id="usr_123",
|
|
91
|
+
image=face_base64,
|
|
92
|
+
action_context="multi_venue_execution",
|
|
93
|
+
duration_seconds=120
|
|
94
|
+
)
|
|
95
|
+
# lock["session_id"], lock["expires_at"]
|
|
96
|
+
|
|
97
|
+
client.lock_heartbeat(lock["session_id"]) # extend
|
|
98
|
+
client.lock_revoke(lock["session_id"]) # revoke
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Analytics
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
client.stats(period="30d")
|
|
105
|
+
client.user_report(user_id="usr_123")
|
|
106
|
+
client.billing_usage()
|
|
107
|
+
client.thresholds() # AI-recommended thresholds
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Error Handling
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from lorica import LoricaClient
|
|
114
|
+
from lorica.client import LoricaError
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
result = client.verify(user_id="usr_123", image=face)
|
|
118
|
+
except LoricaError as e:
|
|
119
|
+
print(e.status_code) # 404
|
|
120
|
+
print(e.error_code) # "user_not_enrolled"
|
|
121
|
+
print(e.message) # "No enrollment found..."
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Links
|
|
125
|
+
|
|
126
|
+
- [Documentation](https://loricaapi.com/docs)
|
|
127
|
+
- [Live Demo](https://loricaapi.com/demo)
|
|
128
|
+
- [Security](https://loricaapi.com/security)
|
|
129
|
+
- [Blog](https://loricaapi.com/blog/)
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lorica
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lorica-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for the Lorica biometric verification API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Tristan Linardos", email = "tristan@loricaapi.com"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["biometric", "verification", "identity", "liveness", "jwt", "api"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Security",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://loricaapi.com"
|
|
32
|
+
Documentation = "https://loricaapi.com/docs"
|
|
33
|
+
Repository = "https://github.com/LeoMirren/lorica"
|
|
34
|
+
Demo = "https://loricaapi.com/demo"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
include = ["lorica*"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Basic tests for the Lorica Python SDK."""
|
|
2
|
+
from lorica import LoricaClient
|
|
3
|
+
|
|
4
|
+
def test_init():
|
|
5
|
+
c = LoricaClient("test-key")
|
|
6
|
+
assert c.api_key == "test-key"
|
|
7
|
+
assert c.base_url == LoricaClient.DEFAULT_URL
|
|
8
|
+
assert c.timeout == 30
|
|
9
|
+
|
|
10
|
+
def test_custom_url():
|
|
11
|
+
c = LoricaClient("test-key", base_url="http://localhost:8000")
|
|
12
|
+
assert c.base_url == "http://localhost:8000"
|
|
13
|
+
|
|
14
|
+
def test_health():
|
|
15
|
+
c = LoricaClient("lorica-dev-key-001")
|
|
16
|
+
h = c.health()
|
|
17
|
+
assert h["status"] == "ok"
|
|
18
|
+
assert "version" in h
|
|
19
|
+
print(f"Health: {h['status']} v{h['version']}")
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
test_init()
|
|
23
|
+
test_custom_url()
|
|
24
|
+
test_health()
|
|
25
|
+
print("All tests passed!")
|