certifieddata-agent-commerce 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.
- certifieddata_agent_commerce-0.1.0/.gitignore +18 -0
- certifieddata_agent_commerce-0.1.0/PKG-INFO +152 -0
- certifieddata_agent_commerce-0.1.0/README.md +124 -0
- certifieddata_agent_commerce-0.1.0/pyproject.toml +46 -0
- certifieddata_agent_commerce-0.1.0/src/certifieddata_agent_commerce/__init__.py +43 -0
- certifieddata_agent_commerce-0.1.0/src/certifieddata_agent_commerce/_http.py +72 -0
- certifieddata_agent_commerce-0.1.0/src/certifieddata_agent_commerce/client.py +90 -0
- certifieddata_agent_commerce-0.1.0/src/certifieddata_agent_commerce/errors.py +80 -0
- certifieddata_agent_commerce-0.1.0/src/certifieddata_agent_commerce/resources/__init__.py +1 -0
- certifieddata_agent_commerce-0.1.0/src/certifieddata_agent_commerce/resources/events.py +29 -0
- certifieddata_agent_commerce-0.1.0/src/certifieddata_agent_commerce/resources/payees.py +136 -0
- certifieddata_agent_commerce-0.1.0/src/certifieddata_agent_commerce/resources/payment_intents.py +75 -0
- certifieddata_agent_commerce-0.1.0/src/certifieddata_agent_commerce/resources/refunds.py +45 -0
- certifieddata_agent_commerce-0.1.0/src/certifieddata_agent_commerce/resources/settlements.py +73 -0
- certifieddata_agent_commerce-0.1.0/src/certifieddata_agent_commerce/resources/transactions.py +110 -0
- certifieddata_agent_commerce-0.1.0/src/certifieddata_agent_commerce/webhooks.py +60 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: certifieddata-agent-commerce
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CertifiedData Agent Commerce Python SDK — provenance-aware payments for AI artifacts
|
|
5
|
+
Project-URL: Homepage, https://certifieddata.io/agent-commerce
|
|
6
|
+
Project-URL: Repository, https://github.com/certifieddata/certifieddata-agent-commerce-public
|
|
7
|
+
Project-URL: Documentation, https://certifieddata.io/agent-commerce/docs
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/certifieddata/certifieddata-agent-commerce-public/issues
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.3; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# certifieddata-agent-commerce
|
|
30
|
+
|
|
31
|
+
Python SDK for [CertifiedData Agent Commerce](https://certifieddata.io/agent-commerce) — provenance-aware, policy-governed payments for AI agents and artifacts.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install certifieddata-agent-commerce
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
> **Note:** Install name is `certifieddata-agent-commerce`. Import name is `certifieddata_agent_commerce` (underscore, as required by Python naming conventions).
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from certifieddata_agent_commerce import CertifiedDataAgentCommerceClient
|
|
45
|
+
|
|
46
|
+
# Reads CDAC_API_KEY and CDAC_BASE_URL from environment automatically
|
|
47
|
+
client = CertifiedDataAgentCommerceClient()
|
|
48
|
+
|
|
49
|
+
# Phase 1 — agent declares intent
|
|
50
|
+
tx = client.transactions.create(
|
|
51
|
+
amount=2500, # cents ($25.00)
|
|
52
|
+
currency="usd",
|
|
53
|
+
rail="stripe",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Phase 2 — attach provenance (binds AI decision record to payment)
|
|
57
|
+
client.transactions.attach_links(
|
|
58
|
+
tx["id"],
|
|
59
|
+
decision_record_id="dec_abc123",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Phase 3+4 — capture: policy eval → settle → inline signed receipt
|
|
63
|
+
capture = client.transactions.capture(tx["id"])
|
|
64
|
+
receipt = capture["receipt"]
|
|
65
|
+
|
|
66
|
+
print(receipt["receipt_id"])
|
|
67
|
+
print(receipt["decision_record_id"])
|
|
68
|
+
print(receipt["sha256_hash"])
|
|
69
|
+
print(receipt["ed25519_sig"])
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Configure
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
export CDAC_API_KEY=cdp_test_xxx
|
|
76
|
+
export CDAC_BASE_URL=https://sandbox.certifieddata.io # omit for live
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
| Environment | Key prefix | Base URL |
|
|
80
|
+
|-------------|--------------|------------------------------------|
|
|
81
|
+
| Sandbox | `cdp_test_` | `https://sandbox.certifieddata.io` |
|
|
82
|
+
| Live | `cdp_live_` | `https://certifieddata.io` |
|
|
83
|
+
|
|
84
|
+
## Verify receipts publicly
|
|
85
|
+
|
|
86
|
+
Verification requires no API key:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
curl https://certifieddata.io/api/payments/verify/<receipt_id>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"valid": true,
|
|
97
|
+
"hashValid": true,
|
|
98
|
+
"signatureValid": true,
|
|
99
|
+
"signingKeyId": "cd_root_2026",
|
|
100
|
+
"signatureAlg": "Ed25519"
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Local development (mock server)
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Start mock server on port 3456
|
|
108
|
+
pip install flask
|
|
109
|
+
python examples/claude-demo/mock_server.py
|
|
110
|
+
|
|
111
|
+
# Run demo against mock
|
|
112
|
+
CDAC_API_KEY=cdp_test_any CDAC_BASE_URL=http://localhost:3456 \
|
|
113
|
+
python examples/claude-demo/demo.py
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Receipt shape
|
|
117
|
+
|
|
118
|
+
Every successful capture returns an inline signed receipt:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"receipt_id": "rcpt_...",
|
|
123
|
+
"transaction_id": "txn_...",
|
|
124
|
+
"policy_id": "pol_...",
|
|
125
|
+
"decision_record_id": "dec_...",
|
|
126
|
+
"sha256_hash": "sha256:...",
|
|
127
|
+
"ed25519_sig": "..."
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`decision_record_id` is the canonical provenance field linking the payment to the AI decision that triggered it.
|
|
132
|
+
|
|
133
|
+
## Package names
|
|
134
|
+
|
|
135
|
+
| Context | Name |
|
|
136
|
+
|---------|------|
|
|
137
|
+
| `pip install` | `certifieddata-agent-commerce` |
|
|
138
|
+
| `import` | `certifieddata_agent_commerce` |
|
|
139
|
+
| Main class | `CertifiedDataAgentCommerceClient` |
|
|
140
|
+
|
|
141
|
+
Python requires hyphens in distribution names and underscores in import names — these are the same package, different naming conventions.
|
|
142
|
+
|
|
143
|
+
## Resources
|
|
144
|
+
|
|
145
|
+
- [Agent Commerce docs](https://certifieddata.io/agent-commerce)
|
|
146
|
+
- [Public repo](https://github.com/certifieddata/certifieddata-agent-commerce-public)
|
|
147
|
+
- [OpenAPI spec](https://github.com/certifieddata/certifieddata-agent-commerce-public/blob/main/openapi/certifieddata-agent-commerce-v1.openapi.yaml)
|
|
148
|
+
- [Receipt verification](https://certifieddata.io/agent-commerce/payment-verification)
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
Apache-2.0
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# certifieddata-agent-commerce
|
|
2
|
+
|
|
3
|
+
Python SDK for [CertifiedData Agent Commerce](https://certifieddata.io/agent-commerce) — provenance-aware, policy-governed payments for AI agents and artifacts.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install certifieddata-agent-commerce
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
> **Note:** Install name is `certifieddata-agent-commerce`. Import name is `certifieddata_agent_commerce` (underscore, as required by Python naming conventions).
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from certifieddata_agent_commerce import CertifiedDataAgentCommerceClient
|
|
17
|
+
|
|
18
|
+
# Reads CDAC_API_KEY and CDAC_BASE_URL from environment automatically
|
|
19
|
+
client = CertifiedDataAgentCommerceClient()
|
|
20
|
+
|
|
21
|
+
# Phase 1 — agent declares intent
|
|
22
|
+
tx = client.transactions.create(
|
|
23
|
+
amount=2500, # cents ($25.00)
|
|
24
|
+
currency="usd",
|
|
25
|
+
rail="stripe",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Phase 2 — attach provenance (binds AI decision record to payment)
|
|
29
|
+
client.transactions.attach_links(
|
|
30
|
+
tx["id"],
|
|
31
|
+
decision_record_id="dec_abc123",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Phase 3+4 — capture: policy eval → settle → inline signed receipt
|
|
35
|
+
capture = client.transactions.capture(tx["id"])
|
|
36
|
+
receipt = capture["receipt"]
|
|
37
|
+
|
|
38
|
+
print(receipt["receipt_id"])
|
|
39
|
+
print(receipt["decision_record_id"])
|
|
40
|
+
print(receipt["sha256_hash"])
|
|
41
|
+
print(receipt["ed25519_sig"])
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Configure
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
export CDAC_API_KEY=cdp_test_xxx
|
|
48
|
+
export CDAC_BASE_URL=https://sandbox.certifieddata.io # omit for live
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
| Environment | Key prefix | Base URL |
|
|
52
|
+
|-------------|--------------|------------------------------------|
|
|
53
|
+
| Sandbox | `cdp_test_` | `https://sandbox.certifieddata.io` |
|
|
54
|
+
| Live | `cdp_live_` | `https://certifieddata.io` |
|
|
55
|
+
|
|
56
|
+
## Verify receipts publicly
|
|
57
|
+
|
|
58
|
+
Verification requires no API key:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
curl https://certifieddata.io/api/payments/verify/<receipt_id>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"valid": true,
|
|
69
|
+
"hashValid": true,
|
|
70
|
+
"signatureValid": true,
|
|
71
|
+
"signingKeyId": "cd_root_2026",
|
|
72
|
+
"signatureAlg": "Ed25519"
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Local development (mock server)
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Start mock server on port 3456
|
|
80
|
+
pip install flask
|
|
81
|
+
python examples/claude-demo/mock_server.py
|
|
82
|
+
|
|
83
|
+
# Run demo against mock
|
|
84
|
+
CDAC_API_KEY=cdp_test_any CDAC_BASE_URL=http://localhost:3456 \
|
|
85
|
+
python examples/claude-demo/demo.py
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Receipt shape
|
|
89
|
+
|
|
90
|
+
Every successful capture returns an inline signed receipt:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"receipt_id": "rcpt_...",
|
|
95
|
+
"transaction_id": "txn_...",
|
|
96
|
+
"policy_id": "pol_...",
|
|
97
|
+
"decision_record_id": "dec_...",
|
|
98
|
+
"sha256_hash": "sha256:...",
|
|
99
|
+
"ed25519_sig": "..."
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`decision_record_id` is the canonical provenance field linking the payment to the AI decision that triggered it.
|
|
104
|
+
|
|
105
|
+
## Package names
|
|
106
|
+
|
|
107
|
+
| Context | Name |
|
|
108
|
+
|---------|------|
|
|
109
|
+
| `pip install` | `certifieddata-agent-commerce` |
|
|
110
|
+
| `import` | `certifieddata_agent_commerce` |
|
|
111
|
+
| Main class | `CertifiedDataAgentCommerceClient` |
|
|
112
|
+
|
|
113
|
+
Python requires hyphens in distribution names and underscores in import names — these are the same package, different naming conventions.
|
|
114
|
+
|
|
115
|
+
## Resources
|
|
116
|
+
|
|
117
|
+
- [Agent Commerce docs](https://certifieddata.io/agent-commerce)
|
|
118
|
+
- [Public repo](https://github.com/certifieddata/certifieddata-agent-commerce-public)
|
|
119
|
+
- [OpenAPI spec](https://github.com/certifieddata/certifieddata-agent-commerce-public/blob/main/openapi/certifieddata-agent-commerce-v1.openapi.yaml)
|
|
120
|
+
- [Receipt verification](https://certifieddata.io/agent-commerce/payment-verification)
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
Apache-2.0
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "certifieddata-agent-commerce"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "CertifiedData Agent Commerce Python SDK — provenance-aware payments for AI artifacts"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"httpx>=0.27.0",
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: Apache Software License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
"Topic :: Office/Business :: Financial",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://certifieddata.io/agent-commerce"
|
|
30
|
+
Repository = "https://github.com/certifieddata/certifieddata-agent-commerce-public"
|
|
31
|
+
Documentation = "https://certifieddata.io/agent-commerce/docs"
|
|
32
|
+
"Bug Tracker" = "https://github.com/certifieddata/certifieddata-agent-commerce-public/issues"
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = ["pytest>=8.0", "pytest-httpx>=0.30", "ruff>=0.3", "mypy>=1.8"]
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/certifieddata_agent_commerce"]
|
|
39
|
+
|
|
40
|
+
[tool.ruff]
|
|
41
|
+
line-length = 100
|
|
42
|
+
target-version = "py39"
|
|
43
|
+
|
|
44
|
+
[tool.mypy]
|
|
45
|
+
python_version = "3.9"
|
|
46
|
+
strict = true
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CertifiedData Agent Commerce Python SDK
|
|
3
|
+
|
|
4
|
+
Provenance-aware payments and settlement for AI artifact commerce.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
from certifieddata_agent_commerce import CertifiedDataAgentCommerceClient
|
|
9
|
+
|
|
10
|
+
client = CertifiedDataAgentCommerceClient(api_key="cdp_test_...")
|
|
11
|
+
|
|
12
|
+
payee = client.payees.create(
|
|
13
|
+
entity_type="company",
|
|
14
|
+
legal_name="Atlas Synthetic Labs, Inc.",
|
|
15
|
+
idempotency_key="create-payee-001",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
See https://github.com/certifieddata/certifieddata-agent-commerce-public for full documentation.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .client import CertifiedDataAgentCommerceClient
|
|
22
|
+
from .errors import (
|
|
23
|
+
CDACError,
|
|
24
|
+
CDACAuthError,
|
|
25
|
+
CDACValidationError,
|
|
26
|
+
CDACNotFoundError,
|
|
27
|
+
CDACConflictError,
|
|
28
|
+
CDACRateLimitError,
|
|
29
|
+
)
|
|
30
|
+
from .webhooks import verify_webhook_signature
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"CertifiedDataAgentCommerceClient",
|
|
34
|
+
"CDACError",
|
|
35
|
+
"CDACAuthError",
|
|
36
|
+
"CDACValidationError",
|
|
37
|
+
"CDACNotFoundError",
|
|
38
|
+
"CDACConflictError",
|
|
39
|
+
"CDACRateLimitError",
|
|
40
|
+
"verify_webhook_signature",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Internal HTTP client wrapper using httpx."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
import httpx
|
|
5
|
+
from .errors import _raise_for_status
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HttpClient:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
api_key: str,
|
|
12
|
+
base_url: str = "https://api.certifieddata.io",
|
|
13
|
+
api_version: str = "2025-01-01",
|
|
14
|
+
timeout: float = 30.0,
|
|
15
|
+
) -> None:
|
|
16
|
+
self._base_url = base_url.rstrip("/")
|
|
17
|
+
self._client = httpx.Client(
|
|
18
|
+
base_url=self._base_url,
|
|
19
|
+
headers={
|
|
20
|
+
"Authorization": f"Bearer {api_key}",
|
|
21
|
+
"CDAC-API-Version": api_version,
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
"User-Agent": "certifieddata-agent-commerce-python/0.1.0",
|
|
24
|
+
},
|
|
25
|
+
timeout=timeout,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def request(
|
|
29
|
+
self,
|
|
30
|
+
method: str,
|
|
31
|
+
path: str,
|
|
32
|
+
*,
|
|
33
|
+
params: Optional[dict[str, Any]] = None,
|
|
34
|
+
json: Optional[Any] = None,
|
|
35
|
+
idempotency_key: Optional[str] = None,
|
|
36
|
+
) -> Any:
|
|
37
|
+
headers: dict[str, str] = {}
|
|
38
|
+
if idempotency_key:
|
|
39
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
40
|
+
|
|
41
|
+
# Remove None values from query params
|
|
42
|
+
if params:
|
|
43
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
44
|
+
|
|
45
|
+
response = self._client.request(
|
|
46
|
+
method,
|
|
47
|
+
path,
|
|
48
|
+
params=params or None,
|
|
49
|
+
json=json,
|
|
50
|
+
headers=headers,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
body: Any = None
|
|
54
|
+
if response.content:
|
|
55
|
+
try:
|
|
56
|
+
body = response.json()
|
|
57
|
+
except Exception:
|
|
58
|
+
body = {"raw": response.text}
|
|
59
|
+
|
|
60
|
+
if not response.is_success:
|
|
61
|
+
_raise_for_status(response.status_code, body)
|
|
62
|
+
|
|
63
|
+
return body
|
|
64
|
+
|
|
65
|
+
def close(self) -> None:
|
|
66
|
+
self._client.close()
|
|
67
|
+
|
|
68
|
+
def __enter__(self) -> "HttpClient":
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
def __exit__(self, *_: Any) -> None:
|
|
72
|
+
self.close()
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""CertifiedData Agent Commerce Python SDK — main client."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from ._http import HttpClient
|
|
7
|
+
from .resources.payees import PayeesResource
|
|
8
|
+
from .resources.payment_intents import PaymentIntentsResource
|
|
9
|
+
from .resources.transactions import TransactionsResource
|
|
10
|
+
from .resources.settlements import SettlementsResource
|
|
11
|
+
from .resources.refunds import RefundsResource
|
|
12
|
+
from .resources.events import EventsResource
|
|
13
|
+
from .webhooks import verify_webhook_signature as _verify_webhook_signature
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CertifiedDataAgentCommerceClient:
|
|
17
|
+
"""
|
|
18
|
+
CertifiedData Agent Commerce API client.
|
|
19
|
+
|
|
20
|
+
Usage::
|
|
21
|
+
|
|
22
|
+
from certifieddata_agent_commerce import CertifiedDataAgentCommerceClient
|
|
23
|
+
|
|
24
|
+
# api_key reads CDAC_API_KEY, base_url reads CDAC_BASE_URL
|
|
25
|
+
client = CertifiedDataAgentCommerceClient()
|
|
26
|
+
|
|
27
|
+
# or pass explicitly:
|
|
28
|
+
client = CertifiedDataAgentCommerceClient(
|
|
29
|
+
api_key="cdp_test_...",
|
|
30
|
+
base_url="https://sandbox.certifieddata.io",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
Use ``cdp_test_`` keys for sandbox, ``cdp_live_`` keys for production.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
*,
|
|
39
|
+
api_key: Optional[str] = None,
|
|
40
|
+
base_url: Optional[str] = None,
|
|
41
|
+
api_version: str = "2025-01-01",
|
|
42
|
+
timeout: float = 30.0,
|
|
43
|
+
) -> None:
|
|
44
|
+
resolved_key = api_key or os.environ.get("CDAC_API_KEY")
|
|
45
|
+
if not resolved_key:
|
|
46
|
+
raise ValueError("api_key is required. Pass it directly or set CDAC_API_KEY.")
|
|
47
|
+
resolved_url = base_url or os.environ.get("CDAC_BASE_URL") or "https://certifieddata.io"
|
|
48
|
+
self._http = HttpClient(
|
|
49
|
+
api_key=resolved_key,
|
|
50
|
+
base_url=resolved_url,
|
|
51
|
+
api_version=api_version,
|
|
52
|
+
timeout=timeout,
|
|
53
|
+
)
|
|
54
|
+
self.payees = PayeesResource(self._http)
|
|
55
|
+
self.payment_intents = PaymentIntentsResource(self._http)
|
|
56
|
+
self.transactions = TransactionsResource(self._http)
|
|
57
|
+
self.settlements = SettlementsResource(self._http)
|
|
58
|
+
self.refunds = RefundsResource(self._http)
|
|
59
|
+
self.events = EventsResource(self._http)
|
|
60
|
+
|
|
61
|
+
def verify_webhook_signature(
|
|
62
|
+
self,
|
|
63
|
+
raw_body: str | bytes,
|
|
64
|
+
signature_header: str,
|
|
65
|
+
timestamp_header: str,
|
|
66
|
+
secret: str,
|
|
67
|
+
tolerance_seconds: int = 300,
|
|
68
|
+
) -> bool:
|
|
69
|
+
"""
|
|
70
|
+
Verify a CDP webhook signature.
|
|
71
|
+
|
|
72
|
+
:param raw_body: Raw request body before any JSON parsing.
|
|
73
|
+
:param signature_header: ``CDAC-Signature`` header value.
|
|
74
|
+
:param timestamp_header: ``CDAC-Timestamp`` header value.
|
|
75
|
+
:param secret: Webhook endpoint secret.
|
|
76
|
+
:param tolerance_seconds: Timestamp tolerance in seconds (default 300).
|
|
77
|
+
"""
|
|
78
|
+
return _verify_webhook_signature(
|
|
79
|
+
raw_body, signature_header, timestamp_header, secret, tolerance_seconds
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def close(self) -> None:
|
|
83
|
+
"""Close the underlying HTTP client."""
|
|
84
|
+
self._http.close()
|
|
85
|
+
|
|
86
|
+
def __enter__(self) -> "CertifiedDataAgentCommerceClient":
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
def __exit__(self, *_: object) -> None:
|
|
90
|
+
self.close()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""CDP error classes. All errors inherit from CDACError."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CDACError(Exception):
|
|
7
|
+
"""Base class for all CertifiedData Agent Commerce SDK errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
message: str,
|
|
12
|
+
*,
|
|
13
|
+
http_status: Optional[int] = None,
|
|
14
|
+
code: Optional[str] = None,
|
|
15
|
+
retryable: bool = False,
|
|
16
|
+
raw: Optional[Any] = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
self.message = message
|
|
20
|
+
self.http_status = http_status
|
|
21
|
+
self.code = code
|
|
22
|
+
self.retryable = retryable
|
|
23
|
+
self.raw = raw
|
|
24
|
+
|
|
25
|
+
def __repr__(self) -> str:
|
|
26
|
+
return f"{self.__class__.__name__}(code={self.code!r}, http_status={self.http_status}, message={self.message!r})"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CDACAuthError(CDACError):
|
|
30
|
+
"""Authentication failed (401/403)."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CDACValidationError(CDACError):
|
|
34
|
+
"""Request validation failed (400)."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, message: str, *, validation_errors: Optional[list[Any]] = None, **kwargs: Any) -> None:
|
|
37
|
+
super().__init__(message, **kwargs)
|
|
38
|
+
self.validation_errors = validation_errors or []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CDACNotFoundError(CDACError):
|
|
42
|
+
"""Resource not found (404)."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CDACConflictError(CDACError):
|
|
46
|
+
"""Conflict — state transition error or idempotency conflict (409)."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CDACRateLimitError(CDACError):
|
|
50
|
+
"""Rate limit exceeded (429). Retryable."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, message: str, *, retry_after: Optional[int] = None, **kwargs: Any) -> None:
|
|
53
|
+
super().__init__(message, retryable=True, **kwargs)
|
|
54
|
+
self.retry_after = retry_after
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _raise_for_status(http_status: int, body: Any) -> None:
|
|
58
|
+
"""Raise an appropriate CDACError from an HTTP status and error body."""
|
|
59
|
+
error = body.get("error", {}) if isinstance(body, dict) else {}
|
|
60
|
+
code = error.get("code", "unknown")
|
|
61
|
+
message = error.get("message", "An unexpected error occurred.")
|
|
62
|
+
retryable = error.get("retryable", False)
|
|
63
|
+
|
|
64
|
+
if http_status == 401 or http_status == 403:
|
|
65
|
+
raise CDACAuthError(message, http_status=http_status, code=code, raw=body)
|
|
66
|
+
if http_status == 400:
|
|
67
|
+
raise CDACValidationError(
|
|
68
|
+
message,
|
|
69
|
+
http_status=http_status,
|
|
70
|
+
code=code,
|
|
71
|
+
raw=body,
|
|
72
|
+
validation_errors=error.get("errors", []),
|
|
73
|
+
)
|
|
74
|
+
if http_status == 404:
|
|
75
|
+
raise CDACNotFoundError(message, http_status=http_status, code=code, raw=body)
|
|
76
|
+
if http_status == 409:
|
|
77
|
+
raise CDACConflictError(message, http_status=http_status, code=code, raw=body)
|
|
78
|
+
if http_status == 429:
|
|
79
|
+
raise CDACRateLimitError(message, http_status=http_status, code=code, raw=body)
|
|
80
|
+
raise CDACError(message, http_status=http_status, code=code, retryable=retryable, raw=body)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Resources package
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Events resource."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
from .._http import HttpClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EventsResource:
|
|
8
|
+
def __init__(self, http: HttpClient) -> None:
|
|
9
|
+
self._http = http
|
|
10
|
+
|
|
11
|
+
def get(self, event_id: str) -> dict[str, Any]:
|
|
12
|
+
return self._http.request("GET", f"/v1/events/{event_id}")
|
|
13
|
+
|
|
14
|
+
def list(
|
|
15
|
+
self,
|
|
16
|
+
*,
|
|
17
|
+
event_type: Optional[str] = None,
|
|
18
|
+
limit: Optional[int] = None,
|
|
19
|
+
starting_after: Optional[str] = None,
|
|
20
|
+
) -> dict[str, Any]:
|
|
21
|
+
return self._http.request(
|
|
22
|
+
"GET",
|
|
23
|
+
"/v1/events",
|
|
24
|
+
params={
|
|
25
|
+
"event_type": event_type,
|
|
26
|
+
"limit": limit,
|
|
27
|
+
"starting_after": starting_after,
|
|
28
|
+
},
|
|
29
|
+
)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Payees resource."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
from .._http import HttpClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PayeesResource:
|
|
8
|
+
def __init__(self, http: HttpClient) -> None:
|
|
9
|
+
self._http = http
|
|
10
|
+
|
|
11
|
+
def create(
|
|
12
|
+
self,
|
|
13
|
+
*,
|
|
14
|
+
entity_type: str,
|
|
15
|
+
legal_name: Optional[str] = None,
|
|
16
|
+
email: Optional[str] = None,
|
|
17
|
+
default_payout_method: Optional[str] = None,
|
|
18
|
+
metadata: Optional[dict[str, str]] = None,
|
|
19
|
+
idempotency_key: Optional[str] = None,
|
|
20
|
+
) -> dict[str, Any]:
|
|
21
|
+
return self._http.request(
|
|
22
|
+
"POST",
|
|
23
|
+
"/v1/payees",
|
|
24
|
+
json={
|
|
25
|
+
"entity_type": entity_type,
|
|
26
|
+
"legal_name": legal_name,
|
|
27
|
+
"email": email,
|
|
28
|
+
"default_payout_method": default_payout_method,
|
|
29
|
+
"metadata": metadata,
|
|
30
|
+
},
|
|
31
|
+
idempotency_key=idempotency_key,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def get(self, payee_id: str) -> dict[str, Any]:
|
|
35
|
+
return self._http.request("GET", f"/v1/payees/{payee_id}")
|
|
36
|
+
|
|
37
|
+
def list(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
limit: Optional[int] = None,
|
|
41
|
+
starting_after: Optional[str] = None,
|
|
42
|
+
) -> dict[str, Any]:
|
|
43
|
+
return self._http.request(
|
|
44
|
+
"GET",
|
|
45
|
+
"/v1/payees",
|
|
46
|
+
params={"limit": limit, "starting_after": starting_after},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def update(
|
|
50
|
+
self,
|
|
51
|
+
payee_id: str,
|
|
52
|
+
*,
|
|
53
|
+
legal_name: Optional[str] = None,
|
|
54
|
+
email: Optional[str] = None,
|
|
55
|
+
default_payout_method: Optional[str] = None,
|
|
56
|
+
metadata: Optional[dict[str, str]] = None,
|
|
57
|
+
idempotency_key: Optional[str] = None,
|
|
58
|
+
) -> dict[str, Any]:
|
|
59
|
+
body = {}
|
|
60
|
+
if legal_name is not None:
|
|
61
|
+
body["legal_name"] = legal_name
|
|
62
|
+
if email is not None:
|
|
63
|
+
body["email"] = email
|
|
64
|
+
if default_payout_method is not None:
|
|
65
|
+
body["default_payout_method"] = default_payout_method
|
|
66
|
+
if metadata is not None:
|
|
67
|
+
body["metadata"] = metadata
|
|
68
|
+
return self._http.request(
|
|
69
|
+
"PATCH",
|
|
70
|
+
f"/v1/payees/{payee_id}",
|
|
71
|
+
json=body,
|
|
72
|
+
idempotency_key=idempotency_key,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def create_alias(
|
|
76
|
+
self,
|
|
77
|
+
payee_id: str,
|
|
78
|
+
*,
|
|
79
|
+
external_system: str,
|
|
80
|
+
external_ref: str,
|
|
81
|
+
idempotency_key: Optional[str] = None,
|
|
82
|
+
) -> dict[str, Any]:
|
|
83
|
+
return self._http.request(
|
|
84
|
+
"POST",
|
|
85
|
+
f"/v1/payees/{payee_id}/aliases",
|
|
86
|
+
json={"external_system": external_system, "external_ref": external_ref},
|
|
87
|
+
idempotency_key=idempotency_key,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def list_aliases(
|
|
91
|
+
self,
|
|
92
|
+
payee_id: str,
|
|
93
|
+
*,
|
|
94
|
+
limit: Optional[int] = None,
|
|
95
|
+
starting_after: Optional[str] = None,
|
|
96
|
+
) -> dict[str, Any]:
|
|
97
|
+
return self._http.request(
|
|
98
|
+
"GET",
|
|
99
|
+
f"/v1/payees/{payee_id}/aliases",
|
|
100
|
+
params={"limit": limit, "starting_after": starting_after},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def create_payout_destination(
|
|
104
|
+
self,
|
|
105
|
+
payee_id: str,
|
|
106
|
+
*,
|
|
107
|
+
rail_type: str,
|
|
108
|
+
is_default: bool = False,
|
|
109
|
+
processor_ref: Optional[str] = None,
|
|
110
|
+
metadata: Optional[dict[str, str]] = None,
|
|
111
|
+
idempotency_key: Optional[str] = None,
|
|
112
|
+
) -> dict[str, Any]:
|
|
113
|
+
return self._http.request(
|
|
114
|
+
"POST",
|
|
115
|
+
f"/v1/payees/{payee_id}/payout-destinations",
|
|
116
|
+
json={
|
|
117
|
+
"rail_type": rail_type,
|
|
118
|
+
"is_default": is_default,
|
|
119
|
+
"processor_ref": processor_ref,
|
|
120
|
+
"metadata": metadata,
|
|
121
|
+
},
|
|
122
|
+
idempotency_key=idempotency_key,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def list_payout_destinations(
|
|
126
|
+
self,
|
|
127
|
+
payee_id: str,
|
|
128
|
+
*,
|
|
129
|
+
limit: Optional[int] = None,
|
|
130
|
+
starting_after: Optional[str] = None,
|
|
131
|
+
) -> dict[str, Any]:
|
|
132
|
+
return self._http.request(
|
|
133
|
+
"GET",
|
|
134
|
+
f"/v1/payees/{payee_id}/payout-destinations",
|
|
135
|
+
params={"limit": limit, "starting_after": starting_after},
|
|
136
|
+
)
|
certifieddata_agent_commerce-0.1.0/src/certifieddata_agent_commerce/resources/payment_intents.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Payment intents resource."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
from .._http import HttpClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PaymentIntentsResource:
|
|
8
|
+
def __init__(self, http: HttpClient) -> None:
|
|
9
|
+
self._http = http
|
|
10
|
+
|
|
11
|
+
def create(
|
|
12
|
+
self,
|
|
13
|
+
*,
|
|
14
|
+
amount: int,
|
|
15
|
+
currency: str,
|
|
16
|
+
rail: str,
|
|
17
|
+
customer_id: Optional[str] = None,
|
|
18
|
+
payee_id: Optional[str] = None,
|
|
19
|
+
description: Optional[str] = None,
|
|
20
|
+
metadata: Optional[dict[str, str]] = None,
|
|
21
|
+
idempotency_key: Optional[str] = None,
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
return self._http.request(
|
|
24
|
+
"POST",
|
|
25
|
+
"/v1/payment-intents",
|
|
26
|
+
json={
|
|
27
|
+
"amount": amount,
|
|
28
|
+
"currency": currency,
|
|
29
|
+
"rail": rail,
|
|
30
|
+
"customer_id": customer_id,
|
|
31
|
+
"payee_id": payee_id,
|
|
32
|
+
"description": description,
|
|
33
|
+
"metadata": metadata,
|
|
34
|
+
},
|
|
35
|
+
idempotency_key=idempotency_key,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def get(self, payment_intent_id: str) -> dict[str, Any]:
|
|
39
|
+
return self._http.request("GET", f"/v1/payment-intents/{payment_intent_id}")
|
|
40
|
+
|
|
41
|
+
def list(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
limit: Optional[int] = None,
|
|
45
|
+
starting_after: Optional[str] = None,
|
|
46
|
+
) -> dict[str, Any]:
|
|
47
|
+
return self._http.request(
|
|
48
|
+
"GET",
|
|
49
|
+
"/v1/payment-intents",
|
|
50
|
+
params={"limit": limit, "starting_after": starting_after},
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def confirm(
|
|
54
|
+
self,
|
|
55
|
+
payment_intent_id: str,
|
|
56
|
+
*,
|
|
57
|
+
idempotency_key: Optional[str] = None,
|
|
58
|
+
) -> dict[str, Any]:
|
|
59
|
+
return self._http.request(
|
|
60
|
+
"POST",
|
|
61
|
+
f"/v1/payment-intents/{payment_intent_id}/confirm",
|
|
62
|
+
idempotency_key=idempotency_key,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def cancel(
|
|
66
|
+
self,
|
|
67
|
+
payment_intent_id: str,
|
|
68
|
+
*,
|
|
69
|
+
idempotency_key: Optional[str] = None,
|
|
70
|
+
) -> dict[str, Any]:
|
|
71
|
+
return self._http.request(
|
|
72
|
+
"POST",
|
|
73
|
+
f"/v1/payment-intents/{payment_intent_id}/cancel",
|
|
74
|
+
idempotency_key=idempotency_key,
|
|
75
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Refunds resource."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
from .._http import HttpClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RefundsResource:
|
|
8
|
+
def __init__(self, http: HttpClient) -> None:
|
|
9
|
+
self._http = http
|
|
10
|
+
|
|
11
|
+
def create(
|
|
12
|
+
self,
|
|
13
|
+
*,
|
|
14
|
+
transaction_id: str,
|
|
15
|
+
amount: int,
|
|
16
|
+
reason: Optional[str] = None,
|
|
17
|
+
metadata: Optional[dict[str, str]] = None,
|
|
18
|
+
idempotency_key: Optional[str] = None,
|
|
19
|
+
) -> dict[str, Any]:
|
|
20
|
+
return self._http.request(
|
|
21
|
+
"POST",
|
|
22
|
+
"/v1/refunds",
|
|
23
|
+
json={
|
|
24
|
+
"transaction_id": transaction_id,
|
|
25
|
+
"amount": amount,
|
|
26
|
+
"reason": reason,
|
|
27
|
+
"metadata": metadata,
|
|
28
|
+
},
|
|
29
|
+
idempotency_key=idempotency_key,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def get(self, refund_id: str) -> dict[str, Any]:
|
|
33
|
+
return self._http.request("GET", f"/v1/refunds/{refund_id}")
|
|
34
|
+
|
|
35
|
+
def list(
|
|
36
|
+
self,
|
|
37
|
+
*,
|
|
38
|
+
limit: Optional[int] = None,
|
|
39
|
+
starting_after: Optional[str] = None,
|
|
40
|
+
) -> dict[str, Any]:
|
|
41
|
+
return self._http.request(
|
|
42
|
+
"GET",
|
|
43
|
+
"/v1/refunds",
|
|
44
|
+
params={"limit": limit, "starting_after": starting_after},
|
|
45
|
+
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Settlements resource."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
from .._http import HttpClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SettlementsResource:
|
|
8
|
+
def __init__(self, http: HttpClient) -> None:
|
|
9
|
+
self._http = http
|
|
10
|
+
|
|
11
|
+
def create(
|
|
12
|
+
self,
|
|
13
|
+
*,
|
|
14
|
+
payee_id: str,
|
|
15
|
+
amount: int,
|
|
16
|
+
currency: str,
|
|
17
|
+
transaction_ids: list[str],
|
|
18
|
+
destination_id: Optional[str] = None,
|
|
19
|
+
metadata: Optional[dict[str, str]] = None,
|
|
20
|
+
idempotency_key: Optional[str] = None,
|
|
21
|
+
) -> dict[str, Any]:
|
|
22
|
+
return self._http.request(
|
|
23
|
+
"POST",
|
|
24
|
+
"/v1/settlements",
|
|
25
|
+
json={
|
|
26
|
+
"payee_id": payee_id,
|
|
27
|
+
"amount": amount,
|
|
28
|
+
"currency": currency,
|
|
29
|
+
"transaction_ids": transaction_ids,
|
|
30
|
+
"destination_id": destination_id,
|
|
31
|
+
"metadata": metadata,
|
|
32
|
+
},
|
|
33
|
+
idempotency_key=idempotency_key,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def get(self, settlement_id: str) -> dict[str, Any]:
|
|
37
|
+
return self._http.request("GET", f"/v1/settlements/{settlement_id}")
|
|
38
|
+
|
|
39
|
+
def list(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
limit: Optional[int] = None,
|
|
43
|
+
starting_after: Optional[str] = None,
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
|
+
return self._http.request(
|
|
46
|
+
"GET",
|
|
47
|
+
"/v1/settlements",
|
|
48
|
+
params={"limit": limit, "starting_after": starting_after},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def submit(
|
|
52
|
+
self,
|
|
53
|
+
settlement_id: str,
|
|
54
|
+
*,
|
|
55
|
+
idempotency_key: Optional[str] = None,
|
|
56
|
+
) -> dict[str, Any]:
|
|
57
|
+
return self._http.request(
|
|
58
|
+
"POST",
|
|
59
|
+
f"/v1/settlements/{settlement_id}/submit",
|
|
60
|
+
idempotency_key=idempotency_key,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def cancel(
|
|
64
|
+
self,
|
|
65
|
+
settlement_id: str,
|
|
66
|
+
*,
|
|
67
|
+
idempotency_key: Optional[str] = None,
|
|
68
|
+
) -> dict[str, Any]:
|
|
69
|
+
return self._http.request(
|
|
70
|
+
"POST",
|
|
71
|
+
f"/v1/settlements/{settlement_id}/cancel",
|
|
72
|
+
idempotency_key=idempotency_key,
|
|
73
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Transactions resource."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
from .._http import HttpClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TransactionsResource:
|
|
8
|
+
def __init__(self, http: HttpClient) -> None:
|
|
9
|
+
self._http = http
|
|
10
|
+
|
|
11
|
+
def create(
|
|
12
|
+
self,
|
|
13
|
+
*,
|
|
14
|
+
amount: int,
|
|
15
|
+
currency: str,
|
|
16
|
+
rail: str,
|
|
17
|
+
payment_intent_id: Optional[str] = None,
|
|
18
|
+
payee_id: Optional[str] = None,
|
|
19
|
+
customer_id: Optional[str] = None,
|
|
20
|
+
description: Optional[str] = None,
|
|
21
|
+
metadata: Optional[dict[str, str]] = None,
|
|
22
|
+
idempotency_key: Optional[str] = None,
|
|
23
|
+
) -> dict[str, Any]:
|
|
24
|
+
return self._http.request(
|
|
25
|
+
"POST",
|
|
26
|
+
"/v1/transactions",
|
|
27
|
+
json={
|
|
28
|
+
"amount": amount,
|
|
29
|
+
"currency": currency,
|
|
30
|
+
"rail": rail,
|
|
31
|
+
"payment_intent_id": payment_intent_id,
|
|
32
|
+
"payee_id": payee_id,
|
|
33
|
+
"customer_id": customer_id,
|
|
34
|
+
"description": description,
|
|
35
|
+
"metadata": metadata,
|
|
36
|
+
},
|
|
37
|
+
idempotency_key=idempotency_key,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def get(self, transaction_id: str) -> dict[str, Any]:
|
|
41
|
+
return self._http.request("GET", f"/v1/transactions/{transaction_id}")
|
|
42
|
+
|
|
43
|
+
def list(
|
|
44
|
+
self,
|
|
45
|
+
*,
|
|
46
|
+
limit: Optional[int] = None,
|
|
47
|
+
starting_after: Optional[str] = None,
|
|
48
|
+
) -> dict[str, Any]:
|
|
49
|
+
return self._http.request(
|
|
50
|
+
"GET",
|
|
51
|
+
"/v1/transactions",
|
|
52
|
+
params={"limit": limit, "starting_after": starting_after},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def attach_links(
|
|
56
|
+
self,
|
|
57
|
+
transaction_id: str,
|
|
58
|
+
*,
|
|
59
|
+
artifact_id: Optional[str] = None,
|
|
60
|
+
certificate_id: Optional[str] = None,
|
|
61
|
+
decision_record_id: Optional[str] = None,
|
|
62
|
+
decision_id: Optional[str] = None,
|
|
63
|
+
dataset_id: Optional[str] = None,
|
|
64
|
+
model_id: Optional[str] = None,
|
|
65
|
+
output_id: Optional[str] = None,
|
|
66
|
+
receipt_hash: Optional[str] = None,
|
|
67
|
+
external_reference: Optional[str] = None,
|
|
68
|
+
provenance_metadata: Optional[dict[str, str]] = None,
|
|
69
|
+
idempotency_key: Optional[str] = None,
|
|
70
|
+
) -> dict[str, Any]:
|
|
71
|
+
return self._http.request(
|
|
72
|
+
"POST",
|
|
73
|
+
f"/v1/transactions/{transaction_id}/attach-links",
|
|
74
|
+
json={
|
|
75
|
+
"artifact_id": artifact_id,
|
|
76
|
+
"certificate_id": certificate_id,
|
|
77
|
+
"decision_record_id": decision_record_id or decision_id,
|
|
78
|
+
"dataset_id": dataset_id,
|
|
79
|
+
"model_id": model_id,
|
|
80
|
+
"output_id": output_id,
|
|
81
|
+
"receipt_hash": receipt_hash,
|
|
82
|
+
"external_reference": external_reference,
|
|
83
|
+
"provenance_metadata": provenance_metadata,
|
|
84
|
+
},
|
|
85
|
+
idempotency_key=idempotency_key,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def capture(
|
|
89
|
+
self,
|
|
90
|
+
transaction_id: str,
|
|
91
|
+
*,
|
|
92
|
+
idempotency_key: Optional[str] = None,
|
|
93
|
+
) -> dict[str, Any]:
|
|
94
|
+
return self._http.request(
|
|
95
|
+
"POST",
|
|
96
|
+
f"/v1/transactions/{transaction_id}/capture",
|
|
97
|
+
idempotency_key=idempotency_key,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def cancel(
|
|
101
|
+
self,
|
|
102
|
+
transaction_id: str,
|
|
103
|
+
*,
|
|
104
|
+
idempotency_key: Optional[str] = None,
|
|
105
|
+
) -> dict[str, Any]:
|
|
106
|
+
return self._http.request(
|
|
107
|
+
"POST",
|
|
108
|
+
f"/v1/transactions/{transaction_id}/cancel",
|
|
109
|
+
idempotency_key=idempotency_key,
|
|
110
|
+
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Webhook signature verification for CDP webhooks."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def verify_webhook_signature(
|
|
9
|
+
raw_body: str | bytes,
|
|
10
|
+
signature_header: str,
|
|
11
|
+
timestamp_header: str,
|
|
12
|
+
secret: str,
|
|
13
|
+
tolerance_seconds: int = 300,
|
|
14
|
+
) -> bool:
|
|
15
|
+
"""
|
|
16
|
+
Verify a CDP webhook signature.
|
|
17
|
+
|
|
18
|
+
CDP signs webhooks using HMAC-SHA256 over the string:
|
|
19
|
+
``{timestamp}.{raw_body}``
|
|
20
|
+
|
|
21
|
+
The ``CDAC-Signature`` header has the format::
|
|
22
|
+
``t={timestamp},v1={hmac_hex}``
|
|
23
|
+
|
|
24
|
+
:param raw_body: Raw request body (str or bytes) — before any JSON parsing.
|
|
25
|
+
:param signature_header: Value of the ``CDAC-Signature`` header.
|
|
26
|
+
:param timestamp_header: Value of the ``CDAC-Timestamp`` header.
|
|
27
|
+
:param secret: Webhook endpoint secret.
|
|
28
|
+
:param tolerance_seconds: Maximum age of the webhook in seconds (default 300).
|
|
29
|
+
:returns: True if the signature is valid and within the timestamp tolerance.
|
|
30
|
+
:raises ValueError: If the signature header format is invalid.
|
|
31
|
+
"""
|
|
32
|
+
# Parse timestamp
|
|
33
|
+
try:
|
|
34
|
+
ts = int(timestamp_header)
|
|
35
|
+
except (ValueError, TypeError) as exc:
|
|
36
|
+
raise ValueError(f"Invalid CDAC-Timestamp header: {timestamp_header!r}") from exc
|
|
37
|
+
|
|
38
|
+
# Check timestamp tolerance
|
|
39
|
+
now = int(time.time())
|
|
40
|
+
if abs(now - ts) > tolerance_seconds:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
# Extract v1 signature from header
|
|
44
|
+
v1_sig: str | None = None
|
|
45
|
+
for part in signature_header.split(","):
|
|
46
|
+
part = part.strip()
|
|
47
|
+
if part.startswith("v1="):
|
|
48
|
+
v1_sig = part[3:]
|
|
49
|
+
break
|
|
50
|
+
|
|
51
|
+
if not v1_sig:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
# Compute expected signature
|
|
55
|
+
body_bytes = raw_body.encode("utf-8") if isinstance(raw_body, str) else raw_body
|
|
56
|
+
signed_payload = f"{ts}.".encode("utf-8") + body_bytes
|
|
57
|
+
expected = hmac.new(secret.encode("utf-8"), signed_payload, hashlib.sha256).hexdigest()
|
|
58
|
+
|
|
59
|
+
# Constant-time comparison
|
|
60
|
+
return hmac.compare_digest(expected, v1_sig)
|