vercre-sdk 1.0.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.
@@ -0,0 +1,43 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ *.egg
9
+ .venv/
10
+ venv/
11
+ .env
12
+ *.pem
13
+ *.key
14
+ .ruff_cache/
15
+ .pytest_cache/
16
+ .mypy_cache/
17
+ .coverage
18
+ htmlcov/
19
+ node_modules/
20
+ artifacts/
21
+ cache/
22
+ nohup.out
23
+
24
+ # sops / age secrets
25
+ keys.txt
26
+ secrets.json
27
+
28
+ # Generated key files (contain private keys — never commit)
29
+ issuer.json
30
+ subject.json
31
+
32
+ # Issued credential files (contain signed JWTs — never commit)
33
+ credential.json
34
+ kyc.json
35
+ aml.json
36
+
37
+ # Frontend
38
+ # frontend/
39
+ .vercel
40
+
41
+ # Database hardening
42
+ certs/
43
+ backups/
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.4
2
+ Name: vercre-sdk
3
+ Version: 1.0.0
4
+ Summary: Python SDK for the VERCRE Verifiable Credentials API
5
+ Project-URL: Homepage, https://vercre.vectorguardlabs.com
6
+ Project-URL: Documentation, https://vercre.vectorguardlabs.com/docs/sdk
7
+ Project-URL: Repository, https://github.com/pavondunbar/Verified-Credentials-SDK
8
+ Project-URL: Issues, https://github.com/pavondunbar/Verified-Credentials-SDK/issues
9
+ Author: Pavon Dunbar
10
+ License-Expression: MIT
11
+ Keywords: api-client,did,sdk,vercre,verifiable-credentials,w3c
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Security :: Cryptography
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: httpx<1.0,>=0.25.0
25
+ Requires-Dist: pydantic<3.0,>=2.0
26
+ Provides-Extra: async
27
+ Requires-Dist: anyio>=4.0; extra == 'async'
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
30
+ Requires-Dist: pytest>=8.0; extra == 'dev'
31
+ Requires-Dist: respx>=0.21; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # vercre-sdk
35
+
36
+ Python SDK for the [VERCRE](https://vercre.vectorguardlabs.com) Verifiable Credentials API.
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ pip install vercre-sdk
42
+ ```
43
+
44
+ ## Quick Start
45
+
46
+ ```python
47
+ from vercre_sdk import VercreClient
48
+
49
+ client = VercreClient(
50
+ api_key="vercre_admin_your_key_here",
51
+ base_url="https://api.vercre.io",
52
+ )
53
+
54
+ # Issue a credential
55
+ credential = client.issue_credential(
56
+ issuer_did="did:key:z6Mk...",
57
+ subject_did="did:key:z6Mp...",
58
+ credential_type="KYCBasic",
59
+ claims={"name": "Alice Johnson", "country": "US"},
60
+ )
61
+ print(f"Issued: {credential.credential_id}")
62
+
63
+ # Verify a credential
64
+ result = client.verify_credential(credential=credential.credential)
65
+ print(f"Valid: {result.valid}, Decision: {result.compliance_decision}")
66
+ ```
67
+
68
+ ## Async Usage
69
+
70
+ ```python
71
+ import asyncio
72
+ from vercre_sdk import AsyncVercreClient
73
+
74
+ async def main():
75
+ async with AsyncVercreClient(api_key="vercre_admin_xxx") as client:
76
+ result = await client.verify_credential(credential=my_credential)
77
+ print(result.compliance_decision)
78
+
79
+ asyncio.run(main())
80
+ ```
81
+
82
+ ## Error Handling
83
+
84
+ ```python
85
+ from vercre_sdk import VercreClient, RateLimitError, AuthenticationError
86
+
87
+ client = VercreClient(api_key="...")
88
+
89
+ try:
90
+ result = client.verify_credential(credential=cred)
91
+ except RateLimitError as e:
92
+ print(f"Rate limited. Retry after {e.retry_after}s")
93
+ except AuthenticationError:
94
+ print("Invalid API key")
95
+ ```
96
+
97
+ ## Available Methods
98
+
99
+ ### Credentials
100
+ - `issue_credential()` - Issue a new verifiable credential
101
+ - `verify_credential()` - Verify a credential with compliance scoring
102
+ - `revoke_credential()` - Revoke a credential
103
+ - `get_credential_status()` - Check on-chain attestation status
104
+ - `get_credential()` - Look up credential by ID
105
+ - `list_credentials()` - List credentials by subject DID
106
+
107
+ ### Verification
108
+ - `verify_presentation()` - Verify a W3C Verifiable Presentation
109
+
110
+ ### Issuers
111
+ - `get_issuer()` - Get issuer details
112
+ - `list_issuers()` - List registered issuers
113
+ - `check_issuer_can_issue()` - Check credential type authorization
114
+
115
+ ### Disputes
116
+ - `file_dispute()` - File a dispute against a credential
117
+ - `get_dispute()` - Get dispute details
118
+ - `list_disputes()` - List disputes
119
+
120
+ ### Reputation
121
+ - `get_reputation()` - Get issuer reputation score
122
+ - `submit_feedback()` - Submit verifier feedback
123
+
124
+ ### Webhooks
125
+ - `register_webhook()` - Register a webhook endpoint
126
+ - `list_webhooks()` - List registered webhooks
127
+ - `delete_webhook()` - Remove a webhook
128
+
129
+ ### Billing
130
+ - `get_subscription()` - Get current plan
131
+ - `get_usage()` - Get usage and remaining quota
132
+ - `subscribe()` - Change billing plan
133
+ - `get_billing_portal_url()` - Self-service billing management
134
+
135
+ ### Keys
136
+ - `rotate_key()` - Rotate signing key for a DID
137
+ - `list_key_versions()` - List key version history
138
+
139
+ ## License
140
+
141
+ MIT
@@ -0,0 +1,108 @@
1
+ # vercre-sdk
2
+
3
+ Python SDK for the [VERCRE](https://vercre.vectorguardlabs.com) Verifiable Credentials API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install vercre-sdk
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from vercre_sdk import VercreClient
15
+
16
+ client = VercreClient(
17
+ api_key="vercre_admin_your_key_here",
18
+ base_url="https://api.vercre.io",
19
+ )
20
+
21
+ # Issue a credential
22
+ credential = client.issue_credential(
23
+ issuer_did="did:key:z6Mk...",
24
+ subject_did="did:key:z6Mp...",
25
+ credential_type="KYCBasic",
26
+ claims={"name": "Alice Johnson", "country": "US"},
27
+ )
28
+ print(f"Issued: {credential.credential_id}")
29
+
30
+ # Verify a credential
31
+ result = client.verify_credential(credential=credential.credential)
32
+ print(f"Valid: {result.valid}, Decision: {result.compliance_decision}")
33
+ ```
34
+
35
+ ## Async Usage
36
+
37
+ ```python
38
+ import asyncio
39
+ from vercre_sdk import AsyncVercreClient
40
+
41
+ async def main():
42
+ async with AsyncVercreClient(api_key="vercre_admin_xxx") as client:
43
+ result = await client.verify_credential(credential=my_credential)
44
+ print(result.compliance_decision)
45
+
46
+ asyncio.run(main())
47
+ ```
48
+
49
+ ## Error Handling
50
+
51
+ ```python
52
+ from vercre_sdk import VercreClient, RateLimitError, AuthenticationError
53
+
54
+ client = VercreClient(api_key="...")
55
+
56
+ try:
57
+ result = client.verify_credential(credential=cred)
58
+ except RateLimitError as e:
59
+ print(f"Rate limited. Retry after {e.retry_after}s")
60
+ except AuthenticationError:
61
+ print("Invalid API key")
62
+ ```
63
+
64
+ ## Available Methods
65
+
66
+ ### Credentials
67
+ - `issue_credential()` - Issue a new verifiable credential
68
+ - `verify_credential()` - Verify a credential with compliance scoring
69
+ - `revoke_credential()` - Revoke a credential
70
+ - `get_credential_status()` - Check on-chain attestation status
71
+ - `get_credential()` - Look up credential by ID
72
+ - `list_credentials()` - List credentials by subject DID
73
+
74
+ ### Verification
75
+ - `verify_presentation()` - Verify a W3C Verifiable Presentation
76
+
77
+ ### Issuers
78
+ - `get_issuer()` - Get issuer details
79
+ - `list_issuers()` - List registered issuers
80
+ - `check_issuer_can_issue()` - Check credential type authorization
81
+
82
+ ### Disputes
83
+ - `file_dispute()` - File a dispute against a credential
84
+ - `get_dispute()` - Get dispute details
85
+ - `list_disputes()` - List disputes
86
+
87
+ ### Reputation
88
+ - `get_reputation()` - Get issuer reputation score
89
+ - `submit_feedback()` - Submit verifier feedback
90
+
91
+ ### Webhooks
92
+ - `register_webhook()` - Register a webhook endpoint
93
+ - `list_webhooks()` - List registered webhooks
94
+ - `delete_webhook()` - Remove a webhook
95
+
96
+ ### Billing
97
+ - `get_subscription()` - Get current plan
98
+ - `get_usage()` - Get usage and remaining quota
99
+ - `subscribe()` - Change billing plan
100
+ - `get_billing_portal_url()` - Self-service billing management
101
+
102
+ ### Keys
103
+ - `rotate_key()` - Rotate signing key for a DID
104
+ - `list_key_versions()` - List key version history
105
+
106
+ ## License
107
+
108
+ MIT
@@ -0,0 +1,52 @@
1
+ [project]
2
+ name = "vercre-sdk"
3
+ version = "1.0.0"
4
+ description = "Python SDK for the VERCRE Verifiable Credentials API"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.10"
8
+ authors = [
9
+ { name = "Pavon Dunbar" },
10
+ ]
11
+ keywords = [
12
+ "verifiable-credentials",
13
+ "w3c",
14
+ "did",
15
+ "sdk",
16
+ "api-client",
17
+ "vercre",
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 4 - Beta",
21
+ "Intended Audience :: Developers",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Topic :: Security :: Cryptography",
29
+ "Topic :: Software Development :: Libraries :: Python Modules",
30
+ "Typing :: Typed",
31
+ ]
32
+ dependencies = [
33
+ "httpx>=0.25.0,<1.0",
34
+ "pydantic>=2.0,<3.0",
35
+ ]
36
+
37
+ [project.optional-dependencies]
38
+ async = ["anyio>=4.0"]
39
+ dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "respx>=0.21"]
40
+
41
+ [project.urls]
42
+ Homepage = "https://vercre.vectorguardlabs.com"
43
+ Documentation = "https://vercre.vectorguardlabs.com/docs/sdk"
44
+ Repository = "https://github.com/pavondunbar/Verified-Credentials-SDK"
45
+ Issues = "https://github.com/pavondunbar/Verified-Credentials-SDK/issues"
46
+
47
+ [build-system]
48
+ requires = ["hatchling"]
49
+ build-backend = "hatchling.build"
50
+
51
+ [tool.hatch.build.targets.wheel]
52
+ packages = ["src/vercre_sdk"]
@@ -0,0 +1,36 @@
1
+ """VERCRE SDK — Python client for the VERCRE Verifiable Credentials API."""
2
+
3
+ from vercre_sdk.async_client import AsyncVercreClient
4
+ from vercre_sdk.client import VercreClient
5
+ from vercre_sdk.exceptions import (
6
+ AuthenticationError,
7
+ NotFoundError,
8
+ RateLimitError,
9
+ ServerError,
10
+ ValidationError,
11
+ VercreError,
12
+ )
13
+ from vercre_sdk.models import (
14
+ Credential,
15
+ Issuer,
16
+ Subscription,
17
+ UsageInfo,
18
+ VerificationResult,
19
+ )
20
+
21
+ __version__ = "1.0.0"
22
+ __all__ = [
23
+ "VercreClient",
24
+ "AsyncVercreClient",
25
+ "VercreError",
26
+ "AuthenticationError",
27
+ "RateLimitError",
28
+ "NotFoundError",
29
+ "ValidationError",
30
+ "ServerError",
31
+ "Credential",
32
+ "VerificationResult",
33
+ "Issuer",
34
+ "Subscription",
35
+ "UsageInfo",
36
+ ]
@@ -0,0 +1,82 @@
1
+ """HTTP transport layer with error handling and retry logic."""
2
+
3
+ import httpx
4
+
5
+ from vercre_sdk.exceptions import (
6
+ AuthenticationError,
7
+ ConflictError,
8
+ ForbiddenError,
9
+ NotFoundError,
10
+ RateLimitError,
11
+ ServerError,
12
+ ValidationError,
13
+ VercreError,
14
+ )
15
+
16
+ _DEFAULT_TIMEOUT = 30.0
17
+ _MAX_RETRIES = 3
18
+ _RETRY_BACKOFF = 1.0
19
+
20
+
21
+ def _raise_for_status(response: httpx.Response) -> None:
22
+ """Raise appropriate SDK exception for error HTTP responses."""
23
+ if response.status_code < 400:
24
+ return
25
+
26
+ try:
27
+ body = response.json()
28
+ detail = body.get("detail", response.text)
29
+ except Exception:
30
+ detail = response.text
31
+
32
+ status = response.status_code
33
+ msg = f"VERCRE API error ({status}): {detail}"
34
+
35
+ if status == 401:
36
+ raise AuthenticationError(msg, status_code=status, detail=detail)
37
+ if status == 403:
38
+ raise ForbiddenError(msg, status_code=status, detail=detail)
39
+ if status == 404:
40
+ raise NotFoundError(msg, status_code=status, detail=detail)
41
+ if status == 409:
42
+ raise ConflictError(msg, status_code=status, detail=detail)
43
+ if status in (400, 422):
44
+ raise ValidationError(msg, status_code=status, detail=detail)
45
+ if status == 429:
46
+ retry_after = response.headers.get("Retry-After")
47
+ raise RateLimitError(
48
+ msg,
49
+ status_code=status,
50
+ detail=detail,
51
+ retry_after=int(retry_after) if retry_after else None,
52
+ )
53
+ if status >= 500:
54
+ raise ServerError(msg, status_code=status, detail=detail)
55
+ raise VercreError(msg, status_code=status, detail=detail)
56
+
57
+
58
+
59
+ def build_headers(api_key: str) -> dict[str, str]:
60
+ """Build default request headers."""
61
+ return {
62
+ "X-API-Key": api_key,
63
+ "Content-Type": "application/json",
64
+ "User-Agent": "vercre-sdk-python/1.0.0",
65
+ }
66
+
67
+
68
+ def should_retry(status_code: int, attempt: int) -> bool:
69
+ """Determine if a request should be retried."""
70
+ if attempt >= _MAX_RETRIES:
71
+ return False
72
+ return status_code in (429, 502, 503, 504)
73
+
74
+
75
+ def retry_delay(attempt: int, retry_after: str | None = None) -> float:
76
+ """Calculate delay before next retry (exponential backoff)."""
77
+ if retry_after:
78
+ try:
79
+ return float(retry_after)
80
+ except ValueError:
81
+ pass
82
+ return _RETRY_BACKOFF * (2 ** attempt)
@@ -0,0 +1,274 @@
1
+ """Asynchronous VERCRE API client."""
2
+
3
+ import asyncio
4
+ from typing import Any
5
+
6
+ import httpx
7
+
8
+ from vercre_sdk._transport import (
9
+ _DEFAULT_TIMEOUT,
10
+ _raise_for_status,
11
+ build_headers,
12
+ retry_delay,
13
+ should_retry,
14
+ )
15
+ from vercre_sdk.models import (
16
+ Credential,
17
+ CredentialStatus,
18
+ DisputeResult,
19
+ Issuer,
20
+ Subscription,
21
+ UsageInfo,
22
+ VerificationResult,
23
+ WebhookResult,
24
+ )
25
+
26
+
27
+ class AsyncVercreClient:
28
+ """Async client for the VERCRE Verifiable Credentials API.
29
+
30
+ Usage:
31
+ async with AsyncVercreClient(api_key="vercre_admin_xxx") as client:
32
+ cred = await client.issue_credential(
33
+ issuer_did="did:key:z6Mk...",
34
+ subject_did="did:key:z6Mp...",
35
+ credential_type="KYCBasic",
36
+ claims={"name": "Alice"},
37
+ )
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ api_key: str,
43
+ base_url: str = "https://api.vercre.io",
44
+ timeout: float = _DEFAULT_TIMEOUT,
45
+ ) -> None:
46
+ self._api_key = api_key
47
+ self._base_url = base_url.rstrip("/")
48
+ self._timeout = timeout
49
+ self._client = httpx.AsyncClient(
50
+ base_url=self._base_url,
51
+ headers=build_headers(api_key),
52
+ timeout=timeout,
53
+ )
54
+
55
+
56
+ async def close(self) -> None:
57
+ """Close the underlying HTTP connection."""
58
+ await self._client.aclose()
59
+
60
+ async def __aenter__(self):
61
+ return self
62
+
63
+ async def __aexit__(self, *args):
64
+ await self.close()
65
+
66
+ async def _request(self, method: str, path: str, **kwargs) -> dict[str, Any]:
67
+ """Make an HTTP request with retry logic."""
68
+ url = f"/api/v1{path}"
69
+ response = None
70
+ for attempt in range(4):
71
+ response = await self._client.request(method, url, **kwargs)
72
+ if should_retry(response.status_code, attempt):
73
+ delay = retry_delay(attempt, response.headers.get("Retry-After"))
74
+ await asyncio.sleep(delay)
75
+ continue
76
+ _raise_for_status(response)
77
+ if response.status_code == 204:
78
+ return {}
79
+ return response.json()
80
+ _raise_for_status(response)
81
+ return response.json()
82
+
83
+ # --- Health ---
84
+
85
+ async def health(self) -> dict:
86
+ """Check API health."""
87
+ return await self._request("GET", "/health")
88
+
89
+ # --- Credentials ---
90
+
91
+ async def issue_credential(
92
+ self,
93
+ issuer_did: str,
94
+ subject_did: str,
95
+ credential_type: str,
96
+ claims: dict,
97
+ expiration: str | None = None,
98
+ ) -> Credential:
99
+ """Issue a new verifiable credential."""
100
+ payload: dict[str, Any] = {
101
+ "issuerDid": issuer_did,
102
+ "subjectDid": subject_did,
103
+ "credentialType": credential_type,
104
+ "claims": claims,
105
+ }
106
+ if expiration:
107
+ payload["expiration"] = expiration
108
+ data = await self._request("POST", "/issue", json=payload)
109
+ return Credential.model_validate(data)
110
+
111
+
112
+ async def verify_credential(
113
+ self,
114
+ credential: dict | None = None,
115
+ credential_id: str | None = None,
116
+ policy: dict | None = None,
117
+ ) -> VerificationResult:
118
+ """Verify a credential and get a compliance decision."""
119
+ payload: dict[str, Any] = {}
120
+ if credential:
121
+ payload["credential"] = credential
122
+ if credential_id:
123
+ payload["credentialId"] = credential_id
124
+ if policy:
125
+ payload["policy"] = policy
126
+ data = await self._request("POST", "/verify", json=payload)
127
+ return VerificationResult.model_validate(data)
128
+
129
+ async def revoke_credential(self, credential: dict, issuer_did: str) -> dict:
130
+ """Revoke a previously issued credential."""
131
+ return await self._request(
132
+ "POST", "/credentials/revoke",
133
+ json={"credential": credential, "issuerDid": issuer_did},
134
+ )
135
+
136
+ async def get_credential_status(self, credential_hash: str) -> CredentialStatus:
137
+ """Get on-chain attestation status."""
138
+ data = await self._request("GET", f"/credentials/{credential_hash}/status")
139
+ return CredentialStatus.model_validate(data)
140
+
141
+ async def get_credential(self, credential_id: str) -> dict:
142
+ """Look up a credential by ID."""
143
+ return await self._request("GET", f"/credentials/{credential_id}/detail")
144
+
145
+ async def list_credentials(self, subject_did: str) -> dict:
146
+ """List all credentials for a subject DID."""
147
+ return await self._request("GET", f"/subjects/{subject_did}/credentials")
148
+
149
+ # --- Verification ---
150
+
151
+ async def verify_presentation(
152
+ self, presentation: dict, check_chain: bool = True
153
+ ) -> dict:
154
+ """Verify a W3C Verifiable Presentation."""
155
+ return await self._request(
156
+ "POST", "/verify-presentation",
157
+ json={"presentation": presentation, "checkChain": check_chain},
158
+ )
159
+
160
+
161
+ # --- Issuers ---
162
+
163
+ async def get_issuer(self, did: str) -> Issuer:
164
+ """Get issuer details."""
165
+ data = await self._request("GET", f"/issuers/{did}")
166
+ return Issuer.model_validate(data)
167
+
168
+ async def list_issuers(self, tier: str | None = None) -> list[dict]:
169
+ """List registered issuers."""
170
+ params = {"tier": tier} if tier else {}
171
+ data = await self._request("GET", "/issuers", params=params)
172
+ return data if isinstance(data, list) else data.get("issuers", [])
173
+
174
+ async def check_issuer_can_issue(self, did: str, credential_type: str) -> dict:
175
+ """Check if an issuer can issue a credential type."""
176
+ return await self._request(
177
+ "GET", f"/issuers/{did}/can-issue", params={"type": credential_type}
178
+ )
179
+
180
+ # --- Disputes ---
181
+
182
+ async def file_dispute(
183
+ self,
184
+ credential_id: str,
185
+ reporter_did: str,
186
+ issuer_did: str,
187
+ reason: str,
188
+ ) -> DisputeResult:
189
+ """File a dispute against a credential."""
190
+ data = await self._request(
191
+ "POST", "/disputes",
192
+ json={
193
+ "credentialId": credential_id,
194
+ "reporterDid": reporter_did,
195
+ "issuerDid": issuer_did,
196
+ "reason": reason,
197
+ },
198
+ )
199
+ return DisputeResult.model_validate(data)
200
+
201
+ async def get_dispute(self, dispute_id: str) -> dict:
202
+ """Get dispute details."""
203
+ return await self._request("GET", f"/disputes/{dispute_id}")
204
+
205
+ async def list_disputes(self, issuer_did: str | None = None) -> list[dict]:
206
+ """List disputes."""
207
+ params = {"issuer": issuer_did} if issuer_did else {}
208
+ data = await self._request("GET", "/disputes", params=params)
209
+ return data if isinstance(data, list) else data.get("disputes", [])
210
+
211
+ # --- Reputation ---
212
+
213
+ async def get_reputation(self, did: str) -> dict:
214
+ """Get an issuer's reputation score."""
215
+ return await self._request("GET", f"/reputation/{did}")
216
+
217
+ async def submit_feedback(self, did: str, positive: bool, detail: str = "") -> dict:
218
+ """Submit verifier feedback on an issuer."""
219
+ return await self._request(
220
+ "POST", f"/reputation/{did}/feedback",
221
+ json={"positive": positive, "detail": detail},
222
+ )
223
+
224
+
225
+ # --- Webhooks ---
226
+
227
+ async def register_webhook(self, url: str, events: list[str]) -> WebhookResult:
228
+ """Register a webhook endpoint."""
229
+ data = await self._request(
230
+ "POST", "/webhooks", json={"url": url, "events": events}
231
+ )
232
+ return WebhookResult.model_validate(data)
233
+
234
+ async def list_webhooks(self) -> list[dict]:
235
+ """List registered webhooks."""
236
+ data = await self._request("GET", "/webhooks")
237
+ return data if isinstance(data, list) else data.get("webhooks", [])
238
+
239
+ async def delete_webhook(self, webhook_id: str) -> dict:
240
+ """Delete a webhook subscription."""
241
+ return await self._request("DELETE", f"/webhooks/{webhook_id}")
242
+
243
+ # --- Billing ---
244
+
245
+ async def get_subscription(self) -> Subscription:
246
+ """Get current billing subscription."""
247
+ data = await self._request("GET", "/billing/subscription")
248
+ return Subscription.model_validate(data)
249
+
250
+ async def get_usage(self) -> UsageInfo:
251
+ """Get current period usage and quotas."""
252
+ data = await self._request("GET", "/billing/usage")
253
+ return UsageInfo.model_validate(data)
254
+
255
+ async def subscribe(self, plan: str) -> dict:
256
+ """Create or change a billing subscription."""
257
+ return await self._request("POST", "/billing/subscribe", json={"plan": plan})
258
+
259
+ async def get_billing_portal_url(self, return_url: str = "") -> str:
260
+ """Get a Stripe billing portal URL."""
261
+ payload = {"returnUrl": return_url} if return_url else {}
262
+ data = await self._request("POST", "/billing/portal", json=payload)
263
+ return data.get("url", "")
264
+
265
+ # --- Keys ---
266
+
267
+ async def rotate_key(self, did: str) -> dict:
268
+ """Rotate the signing key for a DID."""
269
+ return await self._request("POST", f"/keys/{did}/rotate")
270
+
271
+ async def list_key_versions(self, did: str) -> list[dict]:
272
+ """List all key versions for a DID."""
273
+ data = await self._request("GET", f"/keys/{did}/versions")
274
+ return data if isinstance(data, list) else []
@@ -0,0 +1,275 @@
1
+ """Synchronous VERCRE API client."""
2
+
3
+ import time
4
+ from typing import Any
5
+
6
+ import httpx
7
+
8
+ from vercre_sdk._transport import (
9
+ _DEFAULT_TIMEOUT,
10
+ _raise_for_status,
11
+ build_headers,
12
+ retry_delay,
13
+ should_retry,
14
+ )
15
+ from vercre_sdk.models import (
16
+ Credential,
17
+ CredentialStatus,
18
+ DisputeResult,
19
+ Issuer,
20
+ Subscription,
21
+ UsageInfo,
22
+ VerificationResult,
23
+ WebhookResult,
24
+ )
25
+
26
+
27
+ class VercreClient:
28
+ """Synchronous client for the VERCRE Verifiable Credentials API.
29
+
30
+ Usage:
31
+ client = VercreClient(api_key="vercre_admin_xxx", base_url="https://api.vercre.io")
32
+ cred = client.issue_credential(
33
+ issuer_did="did:key:z6Mk...",
34
+ subject_did="did:key:z6Mp...",
35
+ credential_type="KYCBasic",
36
+ claims={"name": "Alice", "country": "US"},
37
+ )
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ api_key: str,
43
+ base_url: str = "https://api.vercre.io",
44
+ timeout: float = _DEFAULT_TIMEOUT,
45
+ ) -> None:
46
+ self._api_key = api_key
47
+ self._base_url = base_url.rstrip("/")
48
+ self._timeout = timeout
49
+ self._client = httpx.Client(
50
+ base_url=self._base_url,
51
+ headers=build_headers(api_key),
52
+ timeout=timeout,
53
+ )
54
+
55
+
56
+ def close(self) -> None:
57
+ """Close the underlying HTTP connection."""
58
+ self._client.close()
59
+
60
+ def __enter__(self):
61
+ return self
62
+
63
+ def __exit__(self, *args):
64
+ self.close()
65
+
66
+ def _request(self, method: str, path: str, **kwargs) -> dict[str, Any]:
67
+ """Make an HTTP request with retry logic."""
68
+ url = f"/api/v1{path}"
69
+ for attempt in range(4):
70
+ response = self._client.request(method, url, **kwargs)
71
+ if should_retry(response.status_code, attempt):
72
+ delay = retry_delay(attempt, response.headers.get("Retry-After"))
73
+ time.sleep(delay)
74
+ continue
75
+ _raise_for_status(response)
76
+ if response.status_code == 204:
77
+ return {}
78
+ return response.json()
79
+ _raise_for_status(response)
80
+ return response.json()
81
+
82
+ # --- Health ---
83
+
84
+ def health(self) -> dict:
85
+ """Check API health."""
86
+ return self._request("GET", "/health")
87
+
88
+ # --- Credentials ---
89
+
90
+ def issue_credential(
91
+ self,
92
+ issuer_did: str,
93
+ subject_did: str,
94
+ credential_type: str,
95
+ claims: dict,
96
+ expiration: str | None = None,
97
+ ) -> Credential:
98
+ """Issue a new verifiable credential."""
99
+ payload: dict[str, Any] = {
100
+ "issuerDid": issuer_did,
101
+ "subjectDid": subject_did,
102
+ "credentialType": credential_type,
103
+ "claims": claims,
104
+ }
105
+ if expiration:
106
+ payload["expiration"] = expiration
107
+ data = self._request("POST", "/issue", json=payload)
108
+ return Credential.model_validate(data)
109
+
110
+
111
+ def verify_credential(
112
+ self,
113
+ credential: dict | None = None,
114
+ credential_id: str | None = None,
115
+ policy: dict | None = None,
116
+ ) -> VerificationResult:
117
+ """Verify a credential and get a compliance decision."""
118
+ payload: dict[str, Any] = {}
119
+ if credential:
120
+ payload["credential"] = credential
121
+ if credential_id:
122
+ payload["credentialId"] = credential_id
123
+ if policy:
124
+ payload["policy"] = policy
125
+ data = self._request("POST", "/verify", json=payload)
126
+ return VerificationResult.model_validate(data)
127
+
128
+ def revoke_credential(self, credential: dict, issuer_did: str) -> dict:
129
+ """Revoke a previously issued credential."""
130
+ return self._request(
131
+ "POST", "/credentials/revoke",
132
+ json={"credential": credential, "issuerDid": issuer_did},
133
+ )
134
+
135
+ def get_credential_status(self, credential_hash: str) -> CredentialStatus:
136
+ """Get on-chain attestation status for a credential."""
137
+ data = self._request("GET", f"/credentials/{credential_hash}/status")
138
+ return CredentialStatus.model_validate(data)
139
+
140
+ def get_credential(self, credential_id: str) -> dict:
141
+ """Look up a credential by ID."""
142
+ return self._request("GET", f"/credentials/{credential_id}/detail")
143
+
144
+ def list_credentials(self, subject_did: str) -> dict:
145
+ """List all credentials for a subject DID."""
146
+ return self._request("GET", f"/subjects/{subject_did}/credentials")
147
+
148
+ # --- Verification ---
149
+
150
+ def verify_presentation(
151
+ self, presentation: dict, check_chain: bool = True
152
+ ) -> dict:
153
+ """Verify a W3C Verifiable Presentation."""
154
+ return self._request(
155
+ "POST", "/verify-presentation",
156
+ json={"presentation": presentation, "checkChain": check_chain},
157
+ )
158
+
159
+
160
+ # --- Issuers ---
161
+
162
+ def get_issuer(self, did: str) -> Issuer:
163
+ """Get issuer details."""
164
+ data = self._request("GET", f"/issuers/{did}")
165
+ return Issuer.model_validate(data)
166
+
167
+ def list_issuers(self, tier: str | None = None) -> list[dict]:
168
+ """List registered issuers."""
169
+ params = {"tier": tier} if tier else {}
170
+ data = self._request("GET", "/issuers", params=params)
171
+ return data if isinstance(data, list) else data.get("issuers", [])
172
+
173
+ def check_issuer_can_issue(self, did: str, credential_type: str) -> dict:
174
+ """Check if an issuer is authorized to issue a credential type."""
175
+ return self._request(
176
+ "GET", f"/issuers/{did}/can-issue",
177
+ params={"type": credential_type},
178
+ )
179
+
180
+ # --- Disputes ---
181
+
182
+ def file_dispute(
183
+ self,
184
+ credential_id: str,
185
+ reporter_did: str,
186
+ issuer_did: str,
187
+ reason: str,
188
+ ) -> DisputeResult:
189
+ """File a dispute against a credential."""
190
+ data = self._request(
191
+ "POST", "/disputes",
192
+ json={
193
+ "credentialId": credential_id,
194
+ "reporterDid": reporter_did,
195
+ "issuerDid": issuer_did,
196
+ "reason": reason,
197
+ },
198
+ )
199
+ return DisputeResult.model_validate(data)
200
+
201
+ def get_dispute(self, dispute_id: str) -> dict:
202
+ """Get dispute details."""
203
+ return self._request("GET", f"/disputes/{dispute_id}")
204
+
205
+ def list_disputes(self, issuer_did: str | None = None) -> list[dict]:
206
+ """List disputes, optionally filtered by issuer."""
207
+ params = {"issuer": issuer_did} if issuer_did else {}
208
+ data = self._request("GET", "/disputes", params=params)
209
+ return data if isinstance(data, list) else data.get("disputes", [])
210
+
211
+
212
+ # --- Reputation ---
213
+
214
+ def get_reputation(self, did: str) -> dict:
215
+ """Get an issuer's reputation score."""
216
+ return self._request("GET", f"/reputation/{did}")
217
+
218
+ def submit_feedback(self, did: str, positive: bool, detail: str = "") -> dict:
219
+ """Submit verifier feedback on an issuer."""
220
+ return self._request(
221
+ "POST", f"/reputation/{did}/feedback",
222
+ json={"positive": positive, "detail": detail},
223
+ )
224
+
225
+ # --- Webhooks ---
226
+
227
+ def register_webhook(self, url: str, events: list[str]) -> WebhookResult:
228
+ """Register a webhook endpoint."""
229
+ data = self._request(
230
+ "POST", "/webhooks",
231
+ json={"url": url, "events": events},
232
+ )
233
+ return WebhookResult.model_validate(data)
234
+
235
+ def list_webhooks(self) -> list[dict]:
236
+ """List registered webhooks."""
237
+ data = self._request("GET", "/webhooks")
238
+ return data if isinstance(data, list) else data.get("webhooks", [])
239
+
240
+ def delete_webhook(self, webhook_id: str) -> dict:
241
+ """Delete a webhook subscription."""
242
+ return self._request("DELETE", f"/webhooks/{webhook_id}")
243
+
244
+ # --- Billing ---
245
+
246
+ def get_subscription(self) -> Subscription:
247
+ """Get current billing subscription."""
248
+ data = self._request("GET", "/billing/subscription")
249
+ return Subscription.model_validate(data)
250
+
251
+ def get_usage(self) -> UsageInfo:
252
+ """Get current period usage and quotas."""
253
+ data = self._request("GET", "/billing/usage")
254
+ return UsageInfo.model_validate(data)
255
+
256
+ def subscribe(self, plan: str) -> dict:
257
+ """Create or change a billing subscription."""
258
+ return self._request("POST", "/billing/subscribe", json={"plan": plan})
259
+
260
+ def get_billing_portal_url(self, return_url: str = "") -> str:
261
+ """Get a Stripe billing portal URL for self-service management."""
262
+ payload = {"returnUrl": return_url} if return_url else {}
263
+ data = self._request("POST", "/billing/portal", json=payload)
264
+ return data.get("url", "")
265
+
266
+ # --- Keys ---
267
+
268
+ def rotate_key(self, did: str) -> dict:
269
+ """Rotate the signing key for a DID."""
270
+ return self._request("POST", f"/keys/{did}/rotate")
271
+
272
+ def list_key_versions(self, did: str) -> list[dict]:
273
+ """List all key versions for a DID."""
274
+ data = self._request("GET", f"/keys/{did}/versions")
275
+ return data if isinstance(data, list) else []
@@ -0,0 +1,46 @@
1
+ """SDK exceptions for VERCRE API errors."""
2
+
3
+
4
+ class VercreError(Exception):
5
+ """Base exception for all VERCRE SDK errors."""
6
+
7
+ def __init__(self, message: str, status_code: int | None = None, detail: str | None = None):
8
+ self.status_code = status_code
9
+ self.detail = detail or message
10
+ super().__init__(message)
11
+
12
+
13
+ class AuthenticationError(VercreError):
14
+ """Raised when API key is missing or invalid (401)."""
15
+
16
+
17
+ class ForbiddenError(VercreError):
18
+ """Raised when the API key lacks permission for the operation (403)."""
19
+
20
+
21
+ class NotFoundError(VercreError):
22
+ """Raised when a requested resource does not exist (404)."""
23
+
24
+
25
+ class ValidationError(VercreError):
26
+ """Raised when request data fails validation (400/422)."""
27
+
28
+
29
+ class RateLimitError(VercreError):
30
+ """Raised when rate limit is exceeded (429).
31
+
32
+ Attributes:
33
+ retry_after: Seconds to wait before retrying.
34
+ """
35
+
36
+ def __init__(self, message: str, retry_after: int | None = None, **kwargs):
37
+ self.retry_after = retry_after
38
+ super().__init__(message, **kwargs)
39
+
40
+
41
+ class ServerError(VercreError):
42
+ """Raised when the VERCRE API returns a 5xx error."""
43
+
44
+
45
+ class ConflictError(VercreError):
46
+ """Raised when the request conflicts with existing state (409)."""
@@ -0,0 +1,121 @@
1
+ """Response models for the VERCRE SDK."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class Credential(BaseModel):
7
+ """An issued verifiable credential."""
8
+
9
+ credential: dict = Field(description="Full signed VC object.")
10
+ credential_hash: str = Field(alias="credentialHash", description="SHA-256 hash.")
11
+ credential_id: str = Field(alias="credentialId", description="Unique ID.")
12
+ transaction_id: str = Field(alias="transactionId", description="On-chain tx ID.")
13
+
14
+
15
+ class IssuerInfo(BaseModel):
16
+ """Issuer details from verification."""
17
+
18
+ did: str
19
+ tier: str
20
+ reputation: int
21
+ status: str
22
+
23
+
24
+ class CredentialInfo(BaseModel):
25
+ """Credential metadata from verification."""
26
+
27
+ type: str
28
+ expired: bool
29
+ revoked: bool
30
+
31
+
32
+ class VerificationResult(BaseModel):
33
+ """Result of a credential verification."""
34
+
35
+ valid: bool
36
+ issuer: IssuerInfo | None = None
37
+ credential: CredentialInfo | None = None
38
+ confidence_score: float | None = Field(None, alias="confidenceScore")
39
+ compliance_decision: str = Field(alias="complianceDecision")
40
+ reasons: list[str] = Field(default_factory=list)
41
+
42
+
43
+
44
+ class Issuer(BaseModel):
45
+ """Registered issuer information."""
46
+
47
+ did: str
48
+ tier: str
49
+ reputation: int
50
+ status: str
51
+ stake: float
52
+ name: str | None = None
53
+
54
+
55
+ class Subscription(BaseModel):
56
+ """Billing subscription state."""
57
+
58
+ plan: str
59
+ active: bool = True
60
+ stripe_customer_id: str | None = Field(None, alias="stripeCustomerId")
61
+ stripe_subscription_id: str | None = Field(None, alias="stripeSubscriptionId")
62
+
63
+
64
+ class UsageInfo(BaseModel):
65
+ """Current period usage information."""
66
+
67
+ plan: str
68
+ # Issuer fields
69
+ issuances: int | None = None
70
+ included_issuances: int | None = Field(None, alias="includedIssuances")
71
+ remaining_issuances: int | None = Field(None, alias="remainingIssuances")
72
+ anchors: int | None = None
73
+ included_anchors: int | None = Field(None, alias="includedAnchors")
74
+ remaining_anchors: int | None = Field(None, alias="remainingAnchors")
75
+ # Verifier fields
76
+ verifications: int | None = None
77
+ included: int | None = None
78
+ remaining: int | None = None
79
+ rate_limit_rpm: int | None = Field(None, alias="rateLimitRpm")
80
+
81
+
82
+ class OnboardResult(BaseModel):
83
+ """Result of onboarding (signup)."""
84
+
85
+ did: str
86
+ name: str
87
+ api_key: str = Field(alias="apiKey")
88
+ api_key_id: str = Field(alias="apiKeyId")
89
+ plan: str | None = None
90
+ tier: str | None = None
91
+ status: str | None = None
92
+
93
+
94
+ class CredentialStatus(BaseModel):
95
+ """On-chain credential status."""
96
+
97
+ status: str
98
+ revoked: bool
99
+ expired: bool
100
+
101
+
102
+ class DisputeResult(BaseModel):
103
+ """Filed dispute result."""
104
+
105
+ id: str
106
+ credential_id: str = Field(alias="credentialId")
107
+ reporter_did: str = Field(alias="reporterDid")
108
+ issuer_did: str = Field(alias="issuerDid")
109
+ reason: str
110
+ status: str
111
+ created_at: str = Field(alias="createdAt")
112
+
113
+
114
+ class WebhookResult(BaseModel):
115
+ """Registered webhook."""
116
+
117
+ id: str
118
+ url: str
119
+ events: list[str]
120
+ created_at: str = Field(alias="createdAt")
121
+ active: bool
File without changes