fype-sdk-beta 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.
- fype_sdk_beta-1.0.0/PKG-INFO +145 -0
- fype_sdk_beta-1.0.0/README.md +130 -0
- fype_sdk_beta-1.0.0/fype/__init__.py +17 -0
- fype_sdk_beta-1.0.0/fype/client.py +110 -0
- fype_sdk_beta-1.0.0/fype/errors.py +37 -0
- fype_sdk_beta-1.0.0/fype/resources.py +234 -0
- fype_sdk_beta-1.0.0/fype_sdk_beta.egg-info/PKG-INFO +145 -0
- fype_sdk_beta-1.0.0/fype_sdk_beta.egg-info/SOURCES.txt +11 -0
- fype_sdk_beta-1.0.0/fype_sdk_beta.egg-info/dependency_links.txt +1 -0
- fype_sdk_beta-1.0.0/fype_sdk_beta.egg-info/requires.txt +5 -0
- fype_sdk_beta-1.0.0/fype_sdk_beta.egg-info/top_level.txt +1 -0
- fype_sdk_beta-1.0.0/pyproject.toml +31 -0
- fype_sdk_beta-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fype-sdk-beta
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for Fype Payments Layer
|
|
5
|
+
Author-email: Fype Engineering <engineering@fype.dev>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: httpx>=0.23.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.20.0; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# Fype Python Client SDK
|
|
17
|
+
|
|
18
|
+
The official Python client library for the [Fype Payments Orchestration Layer](https://fype.dev). Fully type-safe, async-friendly, and lightweight, it simplifies payment creations, refunds, and signed webhook signature verifications under a unified API interface.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
Install the package directly from your local virtual environment:
|
|
25
|
+
```bash
|
|
26
|
+
pip install -e packages/python-sdk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
*(Or via PyPI once distributed)*:
|
|
30
|
+
```bash
|
|
31
|
+
pip install fype
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
### 1. Initialize Client
|
|
39
|
+
Expose your Fype developer API key (use test key for sandbox or live key for production):
|
|
40
|
+
```python
|
|
41
|
+
from fype import Fype
|
|
42
|
+
|
|
43
|
+
# Initialize the client
|
|
44
|
+
fype = Fype(api_key="fype_test_your_secret_api_key_here")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Create Hosted Checkout Session
|
|
48
|
+
Initiate a payment order. Fype will automatically contact gateway adapters under the hood and return a public buyer-facing checkout URL:
|
|
49
|
+
```python
|
|
50
|
+
payment = fype.payments.create(
|
|
51
|
+
amount=25000, # Amount in paise (₹250.00 INR)
|
|
52
|
+
currency="INR",
|
|
53
|
+
customer_email="buyer@email.com",
|
|
54
|
+
success_url="https://yourwebsite.com/payment/success",
|
|
55
|
+
cancel_url="https://yourwebsite.com/payment/cancel",
|
|
56
|
+
reference_id="order_ref_1092", # Optional merchant order ID
|
|
57
|
+
provider="cashfree" # Optional: Explicitly choose gateway (razorpay/cashfree)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
print(f"Transaction ID: {payment['id']}")
|
|
61
|
+
print(f"Checkout URL: {payment['checkout_url']}")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 3. Dynamic Redirect Placeholders
|
|
65
|
+
Like Stripe, Fype natively supports dynamic placeholders inside `success_url` and `cancel_url` redirect strings. This allows you to build frictionless, stateless checkout loops (e.g. for login-free purchases).
|
|
66
|
+
|
|
67
|
+
Fype will automatically replace these placeholders before redirecting the buyer back to your website:
|
|
68
|
+
* **`{CHECKOUT_SESSION_ID}`**: Replaced with the actual Fype Checkout Session UUID.
|
|
69
|
+
* **`{PAYMENT_ID}`**: Replaced with the actual Fype Payment UUID (useful for direct API validation).
|
|
70
|
+
|
|
71
|
+
**Example:**
|
|
72
|
+
```python
|
|
73
|
+
payment = fype.payments.create(
|
|
74
|
+
amount=25000,
|
|
75
|
+
currency="INR",
|
|
76
|
+
customer_email="buyer@email.com",
|
|
77
|
+
success_url="https://yourwebsite.com/payment/success?fype_session_id={PAYMENT_ID}",
|
|
78
|
+
cancel_url="https://yourwebsite.com/payment/cancel"
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 4. Retrieve Payment Details
|
|
85
|
+
Audit checkout session status at any time:
|
|
86
|
+
```python
|
|
87
|
+
payment = fype.payments.retrieve("pay_test_abc123")
|
|
88
|
+
print(f"Current Status: {payment['status']}") # 'created', 'succeeded', 'failed'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 4. Issue a Refund
|
|
92
|
+
Perform partial or full refunds for successful payments:
|
|
93
|
+
```python
|
|
94
|
+
# Full Refund (omit amount parameter)
|
|
95
|
+
refund = fype.refunds.create(payment_id="pay_test_abc123")
|
|
96
|
+
|
|
97
|
+
# Partial Refund (specify amount in paise)
|
|
98
|
+
partial_refund = fype.refunds.create(payment_id="pay_test_abc123", amount=5000)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Webhook Signature Verification
|
|
104
|
+
|
|
105
|
+
Incoming HTTP webhook events are cryptographically signed by Fype. Always verify signatures against your webhook signing secret (`whsec_...`) to prevent spoofing. The SDK handles constant-time HMAC comparison internally to guard against side-channel timing analysis attacks:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from fype.errors import SignatureVerificationError
|
|
109
|
+
|
|
110
|
+
raw_body = request.body() # Get raw request bytes
|
|
111
|
+
signature = request.headers.get("X-Fype-Signature")
|
|
112
|
+
secret = "whsec_your_webhook_signing_secret"
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
fype.webhooks.verify_signature(raw_body, signature, secret)
|
|
116
|
+
print("Webhook signature is valid and authentic!")
|
|
117
|
+
# Proceed to handle events (e.g. payment.succeeded)
|
|
118
|
+
except SignatureVerificationError as e:
|
|
119
|
+
print(f"Webhook signature mismatch: {e}")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Error Handling
|
|
125
|
+
|
|
126
|
+
The SDK exposes explicit, catchable exception classes to let you handle failures gracefully:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from fype.errors import AuthenticationError, InvalidRequestError, ApiConnectionError, FypeError
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
payment = fype.payments.create(...)
|
|
133
|
+
except AuthenticationError:
|
|
134
|
+
# Handle bad API keys
|
|
135
|
+
print("Invalid Fype API key.")
|
|
136
|
+
except InvalidRequestError as e:
|
|
137
|
+
# Handle validation errors (e.g. invalid currency, negative amount)
|
|
138
|
+
print(f"Request failed validation: {e}")
|
|
139
|
+
except ApiConnectionError:
|
|
140
|
+
# Handle network timeouts or unreachable hosts
|
|
141
|
+
print("Connection to Fype server timed out.")
|
|
142
|
+
except FypeError as e:
|
|
143
|
+
# Handle server-side errors
|
|
144
|
+
print(f"Fype gateway error: {e}")
|
|
145
|
+
```
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Fype Python Client SDK
|
|
2
|
+
|
|
3
|
+
The official Python client library for the [Fype Payments Orchestration Layer](https://fype.dev). Fully type-safe, async-friendly, and lightweight, it simplifies payment creations, refunds, and signed webhook signature verifications under a unified API interface.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Install the package directly from your local virtual environment:
|
|
10
|
+
```bash
|
|
11
|
+
pip install -e packages/python-sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
*(Or via PyPI once distributed)*:
|
|
15
|
+
```bash
|
|
16
|
+
pip install fype
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### 1. Initialize Client
|
|
24
|
+
Expose your Fype developer API key (use test key for sandbox or live key for production):
|
|
25
|
+
```python
|
|
26
|
+
from fype import Fype
|
|
27
|
+
|
|
28
|
+
# Initialize the client
|
|
29
|
+
fype = Fype(api_key="fype_test_your_secret_api_key_here")
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 2. Create Hosted Checkout Session
|
|
33
|
+
Initiate a payment order. Fype will automatically contact gateway adapters under the hood and return a public buyer-facing checkout URL:
|
|
34
|
+
```python
|
|
35
|
+
payment = fype.payments.create(
|
|
36
|
+
amount=25000, # Amount in paise (₹250.00 INR)
|
|
37
|
+
currency="INR",
|
|
38
|
+
customer_email="buyer@email.com",
|
|
39
|
+
success_url="https://yourwebsite.com/payment/success",
|
|
40
|
+
cancel_url="https://yourwebsite.com/payment/cancel",
|
|
41
|
+
reference_id="order_ref_1092", # Optional merchant order ID
|
|
42
|
+
provider="cashfree" # Optional: Explicitly choose gateway (razorpay/cashfree)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
print(f"Transaction ID: {payment['id']}")
|
|
46
|
+
print(f"Checkout URL: {payment['checkout_url']}")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 3. Dynamic Redirect Placeholders
|
|
50
|
+
Like Stripe, Fype natively supports dynamic placeholders inside `success_url` and `cancel_url` redirect strings. This allows you to build frictionless, stateless checkout loops (e.g. for login-free purchases).
|
|
51
|
+
|
|
52
|
+
Fype will automatically replace these placeholders before redirecting the buyer back to your website:
|
|
53
|
+
* **`{CHECKOUT_SESSION_ID}`**: Replaced with the actual Fype Checkout Session UUID.
|
|
54
|
+
* **`{PAYMENT_ID}`**: Replaced with the actual Fype Payment UUID (useful for direct API validation).
|
|
55
|
+
|
|
56
|
+
**Example:**
|
|
57
|
+
```python
|
|
58
|
+
payment = fype.payments.create(
|
|
59
|
+
amount=25000,
|
|
60
|
+
currency="INR",
|
|
61
|
+
customer_email="buyer@email.com",
|
|
62
|
+
success_url="https://yourwebsite.com/payment/success?fype_session_id={PAYMENT_ID}",
|
|
63
|
+
cancel_url="https://yourwebsite.com/payment/cancel"
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 4. Retrieve Payment Details
|
|
70
|
+
Audit checkout session status at any time:
|
|
71
|
+
```python
|
|
72
|
+
payment = fype.payments.retrieve("pay_test_abc123")
|
|
73
|
+
print(f"Current Status: {payment['status']}") # 'created', 'succeeded', 'failed'
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 4. Issue a Refund
|
|
77
|
+
Perform partial or full refunds for successful payments:
|
|
78
|
+
```python
|
|
79
|
+
# Full Refund (omit amount parameter)
|
|
80
|
+
refund = fype.refunds.create(payment_id="pay_test_abc123")
|
|
81
|
+
|
|
82
|
+
# Partial Refund (specify amount in paise)
|
|
83
|
+
partial_refund = fype.refunds.create(payment_id="pay_test_abc123", amount=5000)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Webhook Signature Verification
|
|
89
|
+
|
|
90
|
+
Incoming HTTP webhook events are cryptographically signed by Fype. Always verify signatures against your webhook signing secret (`whsec_...`) to prevent spoofing. The SDK handles constant-time HMAC comparison internally to guard against side-channel timing analysis attacks:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from fype.errors import SignatureVerificationError
|
|
94
|
+
|
|
95
|
+
raw_body = request.body() # Get raw request bytes
|
|
96
|
+
signature = request.headers.get("X-Fype-Signature")
|
|
97
|
+
secret = "whsec_your_webhook_signing_secret"
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
fype.webhooks.verify_signature(raw_body, signature, secret)
|
|
101
|
+
print("Webhook signature is valid and authentic!")
|
|
102
|
+
# Proceed to handle events (e.g. payment.succeeded)
|
|
103
|
+
except SignatureVerificationError as e:
|
|
104
|
+
print(f"Webhook signature mismatch: {e}")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Error Handling
|
|
110
|
+
|
|
111
|
+
The SDK exposes explicit, catchable exception classes to let you handle failures gracefully:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from fype.errors import AuthenticationError, InvalidRequestError, ApiConnectionError, FypeError
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
payment = fype.payments.create(...)
|
|
118
|
+
except AuthenticationError:
|
|
119
|
+
# Handle bad API keys
|
|
120
|
+
print("Invalid Fype API key.")
|
|
121
|
+
except InvalidRequestError as e:
|
|
122
|
+
# Handle validation errors (e.g. invalid currency, negative amount)
|
|
123
|
+
print(f"Request failed validation: {e}")
|
|
124
|
+
except ApiConnectionError:
|
|
125
|
+
# Handle network timeouts or unreachable hosts
|
|
126
|
+
print("Connection to Fype server timed out.")
|
|
127
|
+
except FypeError as e:
|
|
128
|
+
# Handle server-side errors
|
|
129
|
+
print(f"Fype gateway error: {e}")
|
|
130
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from fype.client import Fype
|
|
2
|
+
from fype.errors import (
|
|
3
|
+
ApiConnectionError,
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
FypeError,
|
|
6
|
+
InvalidRequestError,
|
|
7
|
+
SignatureVerificationError,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ApiConnectionError",
|
|
12
|
+
"AuthenticationError",
|
|
13
|
+
"Fype",
|
|
14
|
+
"FypeError",
|
|
15
|
+
"InvalidRequestError",
|
|
16
|
+
"SignatureVerificationError",
|
|
17
|
+
]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from fype.errors import ApiConnectionError, AuthenticationError, FypeError, InvalidRequestError
|
|
6
|
+
from fype.resources import (
|
|
7
|
+
EntitlementsResource,
|
|
8
|
+
PaymentsResource,
|
|
9
|
+
PlansResource,
|
|
10
|
+
RefundsResource,
|
|
11
|
+
SubscriptionsResource,
|
|
12
|
+
UsageResource,
|
|
13
|
+
WebhooksResource,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Fype:
|
|
18
|
+
"""
|
|
19
|
+
The official Fype Python client SDK.
|
|
20
|
+
Provides type-safe access to payments, refunds, and webhook verification resources.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, api_key: str, base_url: str = "http://localhost:8000/v1"):
|
|
24
|
+
if not api_key:
|
|
25
|
+
raise ValueError("Fype API Key must be supplied.")
|
|
26
|
+
|
|
27
|
+
self.api_key = api_key
|
|
28
|
+
# Enforce clean suffix formatting on base url
|
|
29
|
+
self.base_url = base_url.rstrip("/")
|
|
30
|
+
|
|
31
|
+
# Initialize resources
|
|
32
|
+
self.payments = PaymentsResource(self)
|
|
33
|
+
self.refunds = RefundsResource(self)
|
|
34
|
+
self.webhooks = WebhooksResource()
|
|
35
|
+
self.subscriptions = SubscriptionsResource(self)
|
|
36
|
+
self.plans = PlansResource(self)
|
|
37
|
+
self.entitlements = EntitlementsResource(self)
|
|
38
|
+
self.usage = UsageResource(self)
|
|
39
|
+
|
|
40
|
+
def _request(
|
|
41
|
+
self,
|
|
42
|
+
method: str,
|
|
43
|
+
path: str,
|
|
44
|
+
params: dict[str, Any] | None = None,
|
|
45
|
+
json: dict[str, Any] | None = None,
|
|
46
|
+
headers: dict[str, str] | None = None,
|
|
47
|
+
) -> Any:
|
|
48
|
+
"""
|
|
49
|
+
Helper handler that signs and executes HTTP request and
|
|
50
|
+
translates responses/exceptions.
|
|
51
|
+
"""
|
|
52
|
+
url = f"{self.base_url}{path}"
|
|
53
|
+
|
|
54
|
+
request_headers = {
|
|
55
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
"User-Agent": "Fype-PythonSDK/v1.0",
|
|
58
|
+
}
|
|
59
|
+
if headers:
|
|
60
|
+
request_headers.update(headers)
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
with httpx.Client(timeout=15.0) as client:
|
|
64
|
+
response = client.request(
|
|
65
|
+
method=method,
|
|
66
|
+
url=url,
|
|
67
|
+
params=params,
|
|
68
|
+
json=json,
|
|
69
|
+
headers=request_headers,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Check for client and server error codes
|
|
73
|
+
if 400 <= response.status_code < 600:
|
|
74
|
+
self._handle_error_response(response)
|
|
75
|
+
|
|
76
|
+
return response.json()
|
|
77
|
+
except httpx.RequestError as e:
|
|
78
|
+
raise ApiConnectionError(
|
|
79
|
+
f"Fype connection error: {e!s}",
|
|
80
|
+
) from e
|
|
81
|
+
|
|
82
|
+
def _handle_error_response(self, response: httpx.Response):
|
|
83
|
+
"""Map HTTP error status codes to dedicated Python exception classes."""
|
|
84
|
+
status_code = response.status_code
|
|
85
|
+
text = response.text
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
error_data = response.json()
|
|
89
|
+
message = error_data.get("message") or error_data.get("detail") or text
|
|
90
|
+
except Exception:
|
|
91
|
+
message = text
|
|
92
|
+
|
|
93
|
+
if status_code == 401:
|
|
94
|
+
raise AuthenticationError(
|
|
95
|
+
f"Authentication failed (API Key is invalid or expired): {message}",
|
|
96
|
+
status_code,
|
|
97
|
+
text,
|
|
98
|
+
)
|
|
99
|
+
elif status_code in (400, 422, 404, 409):
|
|
100
|
+
raise InvalidRequestError(
|
|
101
|
+
f"Invalid request parameters: {message}",
|
|
102
|
+
status_code,
|
|
103
|
+
text,
|
|
104
|
+
)
|
|
105
|
+
else:
|
|
106
|
+
raise FypeError(
|
|
107
|
+
f"Fype server error (HTTP {status_code}): {message}",
|
|
108
|
+
status_code,
|
|
109
|
+
text,
|
|
110
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
class FypeError(Exception):
|
|
2
|
+
"""Base error class for all Fype SDK exceptions."""
|
|
3
|
+
|
|
4
|
+
def __init__(
|
|
5
|
+
self,
|
|
6
|
+
message: str,
|
|
7
|
+
status_code: int | None = None,
|
|
8
|
+
response_body: str | None = None,
|
|
9
|
+
):
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
self.message = message
|
|
12
|
+
self.status_code = status_code
|
|
13
|
+
self.response_body = response_body
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthenticationError(FypeError):
|
|
17
|
+
"""Exception raised when API authentication fails."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InvalidRequestError(FypeError):
|
|
23
|
+
"""Exception raised when the request contains invalid parameters."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ApiConnectionError(FypeError):
|
|
29
|
+
"""Exception raised when communication with the Fype servers fails."""
|
|
30
|
+
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SignatureVerificationError(FypeError):
|
|
35
|
+
"""Exception raised when webhook signature verification fails."""
|
|
36
|
+
|
|
37
|
+
pass
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PaymentsResource:
|
|
7
|
+
"""Helper resource class for creating, fetching, and listing payments."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, client):
|
|
10
|
+
self._client = client
|
|
11
|
+
|
|
12
|
+
def create(
|
|
13
|
+
self,
|
|
14
|
+
amount: int,
|
|
15
|
+
currency: str,
|
|
16
|
+
customer_email: str,
|
|
17
|
+
success_url: str,
|
|
18
|
+
cancel_url: str,
|
|
19
|
+
reference_id: str | None = None,
|
|
20
|
+
metadata: dict[str, Any] | None = None,
|
|
21
|
+
) -> dict[str, Any]:
|
|
22
|
+
"""
|
|
23
|
+
Create a new payment order.
|
|
24
|
+
Returns the created Payment dictionary containing visual checkout_url.
|
|
25
|
+
"""
|
|
26
|
+
payload = {
|
|
27
|
+
"amount": amount,
|
|
28
|
+
"currency": currency,
|
|
29
|
+
"customer_email": customer_email,
|
|
30
|
+
"success_url": success_url,
|
|
31
|
+
"cancel_url": cancel_url,
|
|
32
|
+
}
|
|
33
|
+
if reference_id is not None:
|
|
34
|
+
payload["reference_id"] = reference_id
|
|
35
|
+
if metadata is not None:
|
|
36
|
+
payload["metadata"] = metadata
|
|
37
|
+
|
|
38
|
+
return self._client._request("POST", "/payments", json=payload)
|
|
39
|
+
|
|
40
|
+
def retrieve(self, payment_id: str) -> dict[str, Any]:
|
|
41
|
+
"""Retrieve details for a specific Fype payment ID."""
|
|
42
|
+
return self._client._request("GET", f"/payments/{payment_id}")
|
|
43
|
+
|
|
44
|
+
def list(self, limit: int = 20, offset: int = 0) -> list[dict[str, Any]]:
|
|
45
|
+
"""List paginated payments (separate test/live lists based on key authorization)."""
|
|
46
|
+
params = {"limit": limit, "offset": offset}
|
|
47
|
+
return self._client._request("GET", "/payments", params=params)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class RefundsResource:
|
|
51
|
+
"""Helper resource class for executing refunds."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, client):
|
|
54
|
+
self._client = client
|
|
55
|
+
|
|
56
|
+
def create(self, payment_id: str, amount: int | None = None) -> dict[str, Any]:
|
|
57
|
+
"""
|
|
58
|
+
Initiate a refund for a successful payment.
|
|
59
|
+
If amount is not specified, performs a full refund.
|
|
60
|
+
"""
|
|
61
|
+
payload = {}
|
|
62
|
+
if amount is not None:
|
|
63
|
+
payload["amount"] = amount
|
|
64
|
+
return self._client._request("POST", f"/payments/{payment_id}/refund", json=payload)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class WebhooksResource:
|
|
68
|
+
"""Helper utility for webhook validation and parsing."""
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def verify_signature(raw_payload: bytes, signature_header: str, secret: str) -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Verify incoming webhook X-Fype-Signature header against the configured webhook secret.
|
|
74
|
+
Returns True if signature is valid, raises SignatureVerificationError otherwise.
|
|
75
|
+
"""
|
|
76
|
+
from fype.errors import SignatureVerificationError
|
|
77
|
+
|
|
78
|
+
if not signature_header:
|
|
79
|
+
raise SignatureVerificationError("Webhook signature header is missing.")
|
|
80
|
+
|
|
81
|
+
mac = hmac.new(
|
|
82
|
+
secret.encode("utf-8"),
|
|
83
|
+
msg=raw_payload,
|
|
84
|
+
digestmod=hashlib.sha256,
|
|
85
|
+
)
|
|
86
|
+
expected_signature = mac.hexdigest()
|
|
87
|
+
|
|
88
|
+
if not hmac.compare_digest(expected_signature, signature_header):
|
|
89
|
+
raise SignatureVerificationError("Webhook signature verification failed.")
|
|
90
|
+
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class SubscriptionsResource:
|
|
95
|
+
"""Helper resource class for managing subscriptions."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, client):
|
|
98
|
+
self._client = client
|
|
99
|
+
|
|
100
|
+
def create(
|
|
101
|
+
self,
|
|
102
|
+
customer_id: str,
|
|
103
|
+
plan_id: str,
|
|
104
|
+
price_id: str,
|
|
105
|
+
quantity: int = 1,
|
|
106
|
+
metadata: dict[str, Any] | None = None,
|
|
107
|
+
) -> dict[str, Any]:
|
|
108
|
+
"""Creates a subscription and returns its checkout session or mandate details."""
|
|
109
|
+
payload = {
|
|
110
|
+
"customer_id": customer_id,
|
|
111
|
+
"plan_id": plan_id,
|
|
112
|
+
"price_id": price_id,
|
|
113
|
+
"quantity": quantity,
|
|
114
|
+
}
|
|
115
|
+
if metadata is not None:
|
|
116
|
+
payload["metadata"] = metadata
|
|
117
|
+
return self._client._request("POST", "/subscriptions", json=payload)
|
|
118
|
+
|
|
119
|
+
def retrieve(self, subscription_id: str) -> dict[str, Any]:
|
|
120
|
+
"""Retrieve details for a specific Fype subscription ID."""
|
|
121
|
+
return self._client._request("GET", f"/subscriptions/{subscription_id}")
|
|
122
|
+
|
|
123
|
+
def list(
|
|
124
|
+
self,
|
|
125
|
+
limit: int = 20,
|
|
126
|
+
cursor: str | None = None,
|
|
127
|
+
customer_id: str | None = None,
|
|
128
|
+
) -> dict[str, Any]:
|
|
129
|
+
"""List paginated subscriptions for the merchant, optionally filtered by customer_id."""
|
|
130
|
+
params: dict[str, Any] = {"limit": limit}
|
|
131
|
+
if cursor is not None:
|
|
132
|
+
params["cursor"] = cursor
|
|
133
|
+
if customer_id is not None:
|
|
134
|
+
params["customer_id"] = customer_id
|
|
135
|
+
return self._client._request("GET", "/subscriptions", params=params)
|
|
136
|
+
|
|
137
|
+
def cancel(self, subscription_id: str, cancel_at_period_end: bool = False) -> dict[str, Any]:
|
|
138
|
+
"""Cancels a subscription immediately or at the end of the current period."""
|
|
139
|
+
params = {"cancel_at_period_end": str(cancel_at_period_end).lower()}
|
|
140
|
+
return self._client._request(
|
|
141
|
+
"POST", f"/subscriptions/{subscription_id}/cancel", params=params
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def pause(self, subscription_id: str) -> dict[str, Any]:
|
|
145
|
+
"""Pauses billing cycles for an active subscription."""
|
|
146
|
+
return self._client._request("POST", f"/subscriptions/{subscription_id}/pause")
|
|
147
|
+
|
|
148
|
+
def resume(self, subscription_id: str) -> dict[str, Any]:
|
|
149
|
+
"""Resumes billing cycles for a paused subscription."""
|
|
150
|
+
return self._client._request("POST", f"/subscriptions/{subscription_id}/resume")
|
|
151
|
+
|
|
152
|
+
def change_plan(self, subscription_id: str, plan_id: str, price_id: str) -> dict[str, Any]:
|
|
153
|
+
"""Upgrades or downgrades a subscription plan/price mid-cycle with proration."""
|
|
154
|
+
payload = {"plan_id": plan_id, "price_id": price_id}
|
|
155
|
+
return self._client._request(
|
|
156
|
+
"POST", f"/subscriptions/{subscription_id}/change-plan", json=payload
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def get_events(self, subscription_id: str) -> "list[dict[str, Any]]":
|
|
160
|
+
"""Retrieves the complete state transition history for a subscription."""
|
|
161
|
+
return self._client._request("GET", f"/subscriptions/{subscription_id}/events")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class PlansResource:
|
|
165
|
+
"""Helper resource class for plans catalog management."""
|
|
166
|
+
|
|
167
|
+
def __init__(self, client):
|
|
168
|
+
self._client = client
|
|
169
|
+
|
|
170
|
+
def create(
|
|
171
|
+
self,
|
|
172
|
+
product_id: str,
|
|
173
|
+
name: str,
|
|
174
|
+
description: str | None = None,
|
|
175
|
+
billing_frequency: str = "monthly",
|
|
176
|
+
billing_interval: int = 1,
|
|
177
|
+
trial_period_days: int = 0,
|
|
178
|
+
metadata_json: dict[str, Any] | None = None,
|
|
179
|
+
) -> dict[str, Any]:
|
|
180
|
+
"""Create a new billing plan under a product."""
|
|
181
|
+
payload = {
|
|
182
|
+
"name": name,
|
|
183
|
+
"billing_frequency": billing_frequency,
|
|
184
|
+
"billing_interval": billing_interval,
|
|
185
|
+
"trial_period_days": trial_period_days,
|
|
186
|
+
}
|
|
187
|
+
if description is not None:
|
|
188
|
+
payload["description"] = description
|
|
189
|
+
if metadata_json is not None:
|
|
190
|
+
payload["metadata_json"] = metadata_json
|
|
191
|
+
return self._client._request("POST", f"/products/{product_id}/plans", json=payload)
|
|
192
|
+
|
|
193
|
+
def retrieve(self, plan_id: str) -> dict[str, Any]:
|
|
194
|
+
"""Retrieve a specific plan configuration."""
|
|
195
|
+
return self._client._request("GET", f"/plans/{plan_id}")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class EntitlementsResource:
|
|
199
|
+
"""Helper resource class for verifying feature gates/entitlements."""
|
|
200
|
+
|
|
201
|
+
def __init__(self, client):
|
|
202
|
+
self._client = client
|
|
203
|
+
|
|
204
|
+
def check(self, customer_id: str, entitlement_identifier: str) -> bool:
|
|
205
|
+
"""Verifies if a customer has an active entitlement."""
|
|
206
|
+
res = self._client._request("GET", f"/subscriptions/customer/{customer_id}/entitlements")
|
|
207
|
+
active_entitlements = res.get("active_entitlements", [])
|
|
208
|
+
return entitlement_identifier in active_entitlements
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class UsageResource:
|
|
212
|
+
"""Helper resource class for reporting metered usage."""
|
|
213
|
+
|
|
214
|
+
def __init__(self, client):
|
|
215
|
+
self._client = client
|
|
216
|
+
|
|
217
|
+
def report(
|
|
218
|
+
self,
|
|
219
|
+
subscription_id: str,
|
|
220
|
+
event_type: str,
|
|
221
|
+
quantity: float,
|
|
222
|
+
unit: str = "tokens",
|
|
223
|
+
metadata: dict[str, Any] | None = None,
|
|
224
|
+
) -> dict[str, Any]:
|
|
225
|
+
"""Reports a metered usage event for a subscription."""
|
|
226
|
+
payload = {
|
|
227
|
+
"subscription_id": subscription_id,
|
|
228
|
+
"event_type": event_type,
|
|
229
|
+
"quantity": quantity,
|
|
230
|
+
"unit": unit,
|
|
231
|
+
}
|
|
232
|
+
if metadata is not None:
|
|
233
|
+
payload["metadata"] = metadata
|
|
234
|
+
return self._client._request("POST", "/usage/events", json=payload)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fype-sdk-beta
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for Fype Payments Layer
|
|
5
|
+
Author-email: Fype Engineering <engineering@fype.dev>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: httpx>=0.23.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.20.0; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# Fype Python Client SDK
|
|
17
|
+
|
|
18
|
+
The official Python client library for the [Fype Payments Orchestration Layer](https://fype.dev). Fully type-safe, async-friendly, and lightweight, it simplifies payment creations, refunds, and signed webhook signature verifications under a unified API interface.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
Install the package directly from your local virtual environment:
|
|
25
|
+
```bash
|
|
26
|
+
pip install -e packages/python-sdk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
*(Or via PyPI once distributed)*:
|
|
30
|
+
```bash
|
|
31
|
+
pip install fype
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
### 1. Initialize Client
|
|
39
|
+
Expose your Fype developer API key (use test key for sandbox or live key for production):
|
|
40
|
+
```python
|
|
41
|
+
from fype import Fype
|
|
42
|
+
|
|
43
|
+
# Initialize the client
|
|
44
|
+
fype = Fype(api_key="fype_test_your_secret_api_key_here")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Create Hosted Checkout Session
|
|
48
|
+
Initiate a payment order. Fype will automatically contact gateway adapters under the hood and return a public buyer-facing checkout URL:
|
|
49
|
+
```python
|
|
50
|
+
payment = fype.payments.create(
|
|
51
|
+
amount=25000, # Amount in paise (₹250.00 INR)
|
|
52
|
+
currency="INR",
|
|
53
|
+
customer_email="buyer@email.com",
|
|
54
|
+
success_url="https://yourwebsite.com/payment/success",
|
|
55
|
+
cancel_url="https://yourwebsite.com/payment/cancel",
|
|
56
|
+
reference_id="order_ref_1092", # Optional merchant order ID
|
|
57
|
+
provider="cashfree" # Optional: Explicitly choose gateway (razorpay/cashfree)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
print(f"Transaction ID: {payment['id']}")
|
|
61
|
+
print(f"Checkout URL: {payment['checkout_url']}")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 3. Dynamic Redirect Placeholders
|
|
65
|
+
Like Stripe, Fype natively supports dynamic placeholders inside `success_url` and `cancel_url` redirect strings. This allows you to build frictionless, stateless checkout loops (e.g. for login-free purchases).
|
|
66
|
+
|
|
67
|
+
Fype will automatically replace these placeholders before redirecting the buyer back to your website:
|
|
68
|
+
* **`{CHECKOUT_SESSION_ID}`**: Replaced with the actual Fype Checkout Session UUID.
|
|
69
|
+
* **`{PAYMENT_ID}`**: Replaced with the actual Fype Payment UUID (useful for direct API validation).
|
|
70
|
+
|
|
71
|
+
**Example:**
|
|
72
|
+
```python
|
|
73
|
+
payment = fype.payments.create(
|
|
74
|
+
amount=25000,
|
|
75
|
+
currency="INR",
|
|
76
|
+
customer_email="buyer@email.com",
|
|
77
|
+
success_url="https://yourwebsite.com/payment/success?fype_session_id={PAYMENT_ID}",
|
|
78
|
+
cancel_url="https://yourwebsite.com/payment/cancel"
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 4. Retrieve Payment Details
|
|
85
|
+
Audit checkout session status at any time:
|
|
86
|
+
```python
|
|
87
|
+
payment = fype.payments.retrieve("pay_test_abc123")
|
|
88
|
+
print(f"Current Status: {payment['status']}") # 'created', 'succeeded', 'failed'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 4. Issue a Refund
|
|
92
|
+
Perform partial or full refunds for successful payments:
|
|
93
|
+
```python
|
|
94
|
+
# Full Refund (omit amount parameter)
|
|
95
|
+
refund = fype.refunds.create(payment_id="pay_test_abc123")
|
|
96
|
+
|
|
97
|
+
# Partial Refund (specify amount in paise)
|
|
98
|
+
partial_refund = fype.refunds.create(payment_id="pay_test_abc123", amount=5000)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Webhook Signature Verification
|
|
104
|
+
|
|
105
|
+
Incoming HTTP webhook events are cryptographically signed by Fype. Always verify signatures against your webhook signing secret (`whsec_...`) to prevent spoofing. The SDK handles constant-time HMAC comparison internally to guard against side-channel timing analysis attacks:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from fype.errors import SignatureVerificationError
|
|
109
|
+
|
|
110
|
+
raw_body = request.body() # Get raw request bytes
|
|
111
|
+
signature = request.headers.get("X-Fype-Signature")
|
|
112
|
+
secret = "whsec_your_webhook_signing_secret"
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
fype.webhooks.verify_signature(raw_body, signature, secret)
|
|
116
|
+
print("Webhook signature is valid and authentic!")
|
|
117
|
+
# Proceed to handle events (e.g. payment.succeeded)
|
|
118
|
+
except SignatureVerificationError as e:
|
|
119
|
+
print(f"Webhook signature mismatch: {e}")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Error Handling
|
|
125
|
+
|
|
126
|
+
The SDK exposes explicit, catchable exception classes to let you handle failures gracefully:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from fype.errors import AuthenticationError, InvalidRequestError, ApiConnectionError, FypeError
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
payment = fype.payments.create(...)
|
|
133
|
+
except AuthenticationError:
|
|
134
|
+
# Handle bad API keys
|
|
135
|
+
print("Invalid Fype API key.")
|
|
136
|
+
except InvalidRequestError as e:
|
|
137
|
+
# Handle validation errors (e.g. invalid currency, negative amount)
|
|
138
|
+
print(f"Request failed validation: {e}")
|
|
139
|
+
except ApiConnectionError:
|
|
140
|
+
# Handle network timeouts or unreachable hosts
|
|
141
|
+
print("Connection to Fype server timed out.")
|
|
142
|
+
except FypeError as e:
|
|
143
|
+
# Handle server-side errors
|
|
144
|
+
print(f"Fype gateway error: {e}")
|
|
145
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
fype/__init__.py
|
|
4
|
+
fype/client.py
|
|
5
|
+
fype/errors.py
|
|
6
|
+
fype/resources.py
|
|
7
|
+
fype_sdk_beta.egg-info/PKG-INFO
|
|
8
|
+
fype_sdk_beta.egg-info/SOURCES.txt
|
|
9
|
+
fype_sdk_beta.egg-info/dependency_links.txt
|
|
10
|
+
fype_sdk_beta.egg-info/requires.txt
|
|
11
|
+
fype_sdk_beta.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fype
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fype-sdk-beta"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Official Python SDK for Fype Payments Layer"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Fype Engineering", email = "engineering@fype.dev" }
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"httpx>=0.23.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest>=7.0.0",
|
|
26
|
+
"pytest-asyncio>=0.20.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["."]
|
|
31
|
+
include = ["fype*"]
|