zarpay 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.
- zarpay-1.0.0/LICENSE +21 -0
- zarpay-1.0.0/PKG-INFO +146 -0
- zarpay-1.0.0/README.md +117 -0
- zarpay-1.0.0/pyproject.toml +3 -0
- zarpay-1.0.0/setup.cfg +4 -0
- zarpay-1.0.0/setup.py +22 -0
- zarpay-1.0.0/zarpay/__init__.py +22 -0
- zarpay-1.0.0/zarpay/client.py +227 -0
- zarpay-1.0.0/zarpay.egg-info/PKG-INFO +146 -0
- zarpay-1.0.0/zarpay.egg-info/SOURCES.txt +11 -0
- zarpay-1.0.0/zarpay.egg-info/dependency_links.txt +1 -0
- zarpay-1.0.0/zarpay.egg-info/requires.txt +1 -0
- zarpay-1.0.0/zarpay.egg-info/top_level.txt +1 -0
zarpay-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ZarPay
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
zarpay-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zarpay
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for the ZarPay payment gateway
|
|
5
|
+
Home-page: https://github.com/zarpay/zarpay-python
|
|
6
|
+
Author: ZarPay
|
|
7
|
+
Author-email: support@zarpay.pk
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: zarpay,payments,jazzcash,easypaisa,pakistan
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: requests>=2.20.0
|
|
17
|
+
Dynamic: author
|
|
18
|
+
Dynamic: author-email
|
|
19
|
+
Dynamic: classifier
|
|
20
|
+
Dynamic: description
|
|
21
|
+
Dynamic: description-content-type
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: keywords
|
|
24
|
+
Dynamic: license
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
Dynamic: requires-dist
|
|
27
|
+
Dynamic: requires-python
|
|
28
|
+
Dynamic: summary
|
|
29
|
+
|
|
30
|
+
# ZarPay Python SDK
|
|
31
|
+
|
|
32
|
+
Official Python SDK for the [ZarPay](https://zarpay.pk) payment gateway.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install zarpay
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from zarpay import ZarPay
|
|
44
|
+
|
|
45
|
+
client = ZarPay("sk_sandbox_xxxxxxxxxxxxx")
|
|
46
|
+
|
|
47
|
+
payment = client.payments.create(
|
|
48
|
+
merchant_order_id="ORD-123",
|
|
49
|
+
amount=1500,
|
|
50
|
+
channel_id=1,
|
|
51
|
+
customer_phone="03001234567",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
print(payment["data"]["status"]) # 'completed' | 'processing' | 'failed'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
### List Available Channels
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
channels = client.channels.list()
|
|
63
|
+
|
|
64
|
+
for ch in channels["data"]["channels"]:
|
|
65
|
+
print(f"{ch['id']}: {ch['wallet_type']}")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Create a Payment
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
payment = client.payments.create(
|
|
72
|
+
merchant_order_id="ORD-456",
|
|
73
|
+
amount=2500,
|
|
74
|
+
channel_id=1,
|
|
75
|
+
customer_phone="03001234567",
|
|
76
|
+
metadata={"customer_name": "Ahmed Khan"},
|
|
77
|
+
idempotency_key="unique-key-456",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if payment["success"]:
|
|
81
|
+
print("Payment completed:", payment["data"]["zarpay_id"])
|
|
82
|
+
else:
|
|
83
|
+
print("Payment failed:", payment["data"]["failure_reason"])
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Get Payment Status
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# By ZarPay ID
|
|
90
|
+
payment = client.payments.get("ZP_abc123def456")
|
|
91
|
+
|
|
92
|
+
# By your order ID
|
|
93
|
+
payment = client.payments.get_by_order_id("ORD-456")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Verify Webhooks
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from zarpay import verify_webhook
|
|
100
|
+
|
|
101
|
+
# In your webhook handler (e.g. Flask/Django)
|
|
102
|
+
def webhook_handler(request):
|
|
103
|
+
try:
|
|
104
|
+
event = verify_webhook(
|
|
105
|
+
raw_body=request.body,
|
|
106
|
+
signature_header=request.headers["X-ZarPay-Signature"],
|
|
107
|
+
secret="whsec_your_webhook_secret",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if event["event"] == "payment.completed":
|
|
111
|
+
# Fulfill the order
|
|
112
|
+
pass
|
|
113
|
+
elif event["event"] == "payment.failed":
|
|
114
|
+
# Notify customer
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
return HttpResponse(status=200)
|
|
118
|
+
except ValueError:
|
|
119
|
+
return HttpResponse(status=400)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Error Handling
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from zarpay import ZarPay, ZarPayAPIError
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
payment = client.payments.create(...)
|
|
129
|
+
except ZarPayAPIError as e:
|
|
130
|
+
print(e.status_code) # 400, 401, 409, etc.
|
|
131
|
+
print(e.error) # Human-readable error message
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Configuration
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
client = ZarPay(
|
|
138
|
+
"sk_sandbox_xxx",
|
|
139
|
+
base_url="http://localhost:3000/api/v1", # local development
|
|
140
|
+
timeout=60, # seconds
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT
|
zarpay-1.0.0/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# ZarPay Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [ZarPay](https://zarpay.pk) payment gateway.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install zarpay
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from zarpay import ZarPay
|
|
15
|
+
|
|
16
|
+
client = ZarPay("sk_sandbox_xxxxxxxxxxxxx")
|
|
17
|
+
|
|
18
|
+
payment = client.payments.create(
|
|
19
|
+
merchant_order_id="ORD-123",
|
|
20
|
+
amount=1500,
|
|
21
|
+
channel_id=1,
|
|
22
|
+
customer_phone="03001234567",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
print(payment["data"]["status"]) # 'completed' | 'processing' | 'failed'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### List Available Channels
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
channels = client.channels.list()
|
|
34
|
+
|
|
35
|
+
for ch in channels["data"]["channels"]:
|
|
36
|
+
print(f"{ch['id']}: {ch['wallet_type']}")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Create a Payment
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
payment = client.payments.create(
|
|
43
|
+
merchant_order_id="ORD-456",
|
|
44
|
+
amount=2500,
|
|
45
|
+
channel_id=1,
|
|
46
|
+
customer_phone="03001234567",
|
|
47
|
+
metadata={"customer_name": "Ahmed Khan"},
|
|
48
|
+
idempotency_key="unique-key-456",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if payment["success"]:
|
|
52
|
+
print("Payment completed:", payment["data"]["zarpay_id"])
|
|
53
|
+
else:
|
|
54
|
+
print("Payment failed:", payment["data"]["failure_reason"])
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Get Payment Status
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# By ZarPay ID
|
|
61
|
+
payment = client.payments.get("ZP_abc123def456")
|
|
62
|
+
|
|
63
|
+
# By your order ID
|
|
64
|
+
payment = client.payments.get_by_order_id("ORD-456")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Verify Webhooks
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from zarpay import verify_webhook
|
|
71
|
+
|
|
72
|
+
# In your webhook handler (e.g. Flask/Django)
|
|
73
|
+
def webhook_handler(request):
|
|
74
|
+
try:
|
|
75
|
+
event = verify_webhook(
|
|
76
|
+
raw_body=request.body,
|
|
77
|
+
signature_header=request.headers["X-ZarPay-Signature"],
|
|
78
|
+
secret="whsec_your_webhook_secret",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if event["event"] == "payment.completed":
|
|
82
|
+
# Fulfill the order
|
|
83
|
+
pass
|
|
84
|
+
elif event["event"] == "payment.failed":
|
|
85
|
+
# Notify customer
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
return HttpResponse(status=200)
|
|
89
|
+
except ValueError:
|
|
90
|
+
return HttpResponse(status=400)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Error Handling
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from zarpay import ZarPay, ZarPayAPIError
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
payment = client.payments.create(...)
|
|
100
|
+
except ZarPayAPIError as e:
|
|
101
|
+
print(e.status_code) # 400, 401, 409, etc.
|
|
102
|
+
print(e.error) # Human-readable error message
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Configuration
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
client = ZarPay(
|
|
109
|
+
"sk_sandbox_xxx",
|
|
110
|
+
base_url="http://localhost:3000/api/v1", # local development
|
|
111
|
+
timeout=60, # seconds
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
zarpay-1.0.0/setup.cfg
ADDED
zarpay-1.0.0/setup.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="zarpay",
|
|
5
|
+
version="1.0.0",
|
|
6
|
+
description="Official Python SDK for the ZarPay payment gateway",
|
|
7
|
+
long_description=open("README.md").read(),
|
|
8
|
+
long_description_content_type="text/markdown",
|
|
9
|
+
author="ZarPay",
|
|
10
|
+
author_email="support@zarpay.pk",
|
|
11
|
+
url="https://github.com/zarpay/zarpay-python",
|
|
12
|
+
packages=find_packages(),
|
|
13
|
+
python_requires=">=3.8",
|
|
14
|
+
install_requires=["requests>=2.20.0"],
|
|
15
|
+
keywords=["zarpay", "payments", "jazzcash", "easypaisa", "pakistan"],
|
|
16
|
+
license="MIT",
|
|
17
|
+
classifiers=[
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
],
|
|
22
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ZarPay Python SDK
|
|
3
|
+
|
|
4
|
+
Official SDK for integrating with the ZarPay payment gateway.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from zarpay import ZarPay
|
|
8
|
+
|
|
9
|
+
client = ZarPay("sk_sandbox_xxxxxxxxxxxxx")
|
|
10
|
+
|
|
11
|
+
payment = client.payments.create(
|
|
12
|
+
merchant_order_id="ORD-123",
|
|
13
|
+
amount=1500,
|
|
14
|
+
channel_id=1,
|
|
15
|
+
customer_phone="03001234567",
|
|
16
|
+
)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .client import ZarPay, ZarPayAPIError, verify_webhook
|
|
20
|
+
|
|
21
|
+
__version__ = "1.0.0"
|
|
22
|
+
__all__ = ["ZarPay", "ZarPayAPIError", "verify_webhook"]
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ZarPay Python SDK — Client implementation
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
from urllib.parse import quote
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
DEFAULT_BASE_URL = "https://zarpay.pk/api/v1"
|
|
16
|
+
DEFAULT_TIMEOUT = 120
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ZarPayAPIError(Exception):
|
|
20
|
+
"""Raised when the ZarPay API returns an error response."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, status_code: int, error: str, body: dict):
|
|
23
|
+
self.status_code = status_code
|
|
24
|
+
self.error = error
|
|
25
|
+
self.body = body
|
|
26
|
+
super().__init__(f"ZarPay API error ({status_code}): {error}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PaymentsResource:
|
|
30
|
+
"""Payment operations."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, client: "ZarPay"):
|
|
33
|
+
self._client = client
|
|
34
|
+
|
|
35
|
+
def create(
|
|
36
|
+
self,
|
|
37
|
+
merchant_order_id: str,
|
|
38
|
+
amount: float,
|
|
39
|
+
channel_id: int,
|
|
40
|
+
customer_phone: str,
|
|
41
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
42
|
+
idempotency_key: Optional[str] = None,
|
|
43
|
+
) -> dict:
|
|
44
|
+
"""
|
|
45
|
+
Create a new payment.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
merchant_order_id: Your unique order ID (max 100 chars)
|
|
49
|
+
amount: Amount in PKR (minimum 100)
|
|
50
|
+
channel_id: Channel ID from channels.list()
|
|
51
|
+
customer_phone: Customer phone (03XXXXXXXXX or +923XXXXXXXXX)
|
|
52
|
+
metadata: Optional custom key-value pairs
|
|
53
|
+
idempotency_key: Optional key to prevent duplicates
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Payment response dict with 'success' and 'data' keys
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ZarPayAPIError: If the API returns an error
|
|
60
|
+
"""
|
|
61
|
+
body: Dict[str, Any] = {
|
|
62
|
+
"merchant_order_id": merchant_order_id,
|
|
63
|
+
"amount": amount,
|
|
64
|
+
"channel_id": channel_id,
|
|
65
|
+
"customer_phone": customer_phone,
|
|
66
|
+
}
|
|
67
|
+
if metadata is not None:
|
|
68
|
+
body["metadata"] = metadata
|
|
69
|
+
if idempotency_key is not None:
|
|
70
|
+
body["idempotency_key"] = idempotency_key
|
|
71
|
+
|
|
72
|
+
return self._client._request("POST", "/payments", json=body)
|
|
73
|
+
|
|
74
|
+
def get(self, zarpay_id: str) -> dict:
|
|
75
|
+
"""
|
|
76
|
+
Get a payment by its ZarPay ID.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
zarpay_id: The ZarPay-assigned payment ID (ZP_xxx)
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Payment response dict
|
|
83
|
+
"""
|
|
84
|
+
return self._client._request("GET", f"/payments/{quote(zarpay_id, safe='')}")
|
|
85
|
+
|
|
86
|
+
def get_by_order_id(self, order_id: str) -> dict:
|
|
87
|
+
"""
|
|
88
|
+
Get a payment by your merchant order ID.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
order_id: Your merchant_order_id
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Payment response dict
|
|
95
|
+
"""
|
|
96
|
+
return self._client._request(
|
|
97
|
+
"GET", f"/payments/by-order/{quote(order_id, safe='')}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ChannelsResource:
|
|
102
|
+
"""Channel operations."""
|
|
103
|
+
|
|
104
|
+
def __init__(self, client: "ZarPay"):
|
|
105
|
+
self._client = client
|
|
106
|
+
|
|
107
|
+
def list(self) -> dict:
|
|
108
|
+
"""
|
|
109
|
+
List available payment channels for your project.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Channels response with payment_methods, channels, and mode
|
|
113
|
+
"""
|
|
114
|
+
return self._client._request("GET", "/channels")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class ZarPay:
|
|
118
|
+
"""
|
|
119
|
+
ZarPay API client.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
api_key: Your ZarPay API key (sk_sandbox_... or sk_production_...)
|
|
123
|
+
base_url: API base URL (default: https://zarpay.pk/api/v1)
|
|
124
|
+
timeout: Request timeout in seconds (default: 120)
|
|
125
|
+
|
|
126
|
+
Example::
|
|
127
|
+
|
|
128
|
+
from zarpay import ZarPay
|
|
129
|
+
|
|
130
|
+
client = ZarPay("sk_sandbox_xxxxxxxxxxxxx")
|
|
131
|
+
|
|
132
|
+
# List channels
|
|
133
|
+
channels = client.channels.list()
|
|
134
|
+
|
|
135
|
+
# Create payment
|
|
136
|
+
payment = client.payments.create(
|
|
137
|
+
merchant_order_id="ORD-123",
|
|
138
|
+
amount=1500,
|
|
139
|
+
channel_id=1,
|
|
140
|
+
customer_phone="03001234567",
|
|
141
|
+
)
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def __init__(
|
|
145
|
+
self,
|
|
146
|
+
api_key: str,
|
|
147
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
148
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
149
|
+
):
|
|
150
|
+
if not api_key:
|
|
151
|
+
raise ValueError("ZarPay API key is required")
|
|
152
|
+
|
|
153
|
+
self._api_key = api_key
|
|
154
|
+
self._base_url = base_url.rstrip("/")
|
|
155
|
+
self._timeout = timeout
|
|
156
|
+
self._session = requests.Session()
|
|
157
|
+
self._session.headers.update(
|
|
158
|
+
{
|
|
159
|
+
"Authorization": f"Bearer {api_key}",
|
|
160
|
+
"Content-Type": "application/json",
|
|
161
|
+
"User-Agent": "zarpay-python/1.0.0",
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
self.payments = PaymentsResource(self)
|
|
166
|
+
self.channels = ChannelsResource(self)
|
|
167
|
+
|
|
168
|
+
def _request(self, method: str, path: str, **kwargs) -> dict:
|
|
169
|
+
url = f"{self._base_url}{path}"
|
|
170
|
+
response = self._session.request(
|
|
171
|
+
method, url, timeout=self._timeout, **kwargs
|
|
172
|
+
)
|
|
173
|
+
data = response.json()
|
|
174
|
+
|
|
175
|
+
if not response.ok and "error" in data:
|
|
176
|
+
raise ZarPayAPIError(response.status_code, data["error"], data)
|
|
177
|
+
|
|
178
|
+
return data
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def verify_webhook(
|
|
182
|
+
raw_body: bytes,
|
|
183
|
+
signature_header: str,
|
|
184
|
+
secret: str,
|
|
185
|
+
tolerance_sec: int = 300,
|
|
186
|
+
) -> dict:
|
|
187
|
+
"""
|
|
188
|
+
Verify a ZarPay webhook signature.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
raw_body: The raw request body (bytes)
|
|
192
|
+
signature_header: The X-ZarPay-Signature header value
|
|
193
|
+
secret: Your webhook signing secret (whsec_...)
|
|
194
|
+
tolerance_sec: Max age in seconds (default: 300)
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
The parsed webhook payload dict
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
ValueError: If signature is invalid or timestamp is too old
|
|
201
|
+
"""
|
|
202
|
+
parts = {}
|
|
203
|
+
for part in signature_header.split(","):
|
|
204
|
+
key, _, value = part.partition("=")
|
|
205
|
+
parts[key] = value
|
|
206
|
+
|
|
207
|
+
t = parts.get("t", "")
|
|
208
|
+
v1 = parts.get("v1", "")
|
|
209
|
+
|
|
210
|
+
if not t or not v1:
|
|
211
|
+
raise ValueError("Invalid X-ZarPay-Signature header format")
|
|
212
|
+
|
|
213
|
+
timestamp = int(t)
|
|
214
|
+
if abs(time.time() - timestamp) > tolerance_sec:
|
|
215
|
+
raise ValueError("Webhook timestamp too old — possible replay attack")
|
|
216
|
+
|
|
217
|
+
body_str = raw_body.decode("utf-8") if isinstance(raw_body, bytes) else raw_body
|
|
218
|
+
expected = hmac.new(
|
|
219
|
+
secret.encode("utf-8"),
|
|
220
|
+
f"{t}.{body_str}".encode("utf-8"),
|
|
221
|
+
digestmod=hashlib.sha256,
|
|
222
|
+
).hexdigest()
|
|
223
|
+
|
|
224
|
+
if not hmac.compare_digest(expected, v1):
|
|
225
|
+
raise ValueError("Invalid webhook signature")
|
|
226
|
+
|
|
227
|
+
return json.loads(raw_body)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zarpay
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for the ZarPay payment gateway
|
|
5
|
+
Home-page: https://github.com/zarpay/zarpay-python
|
|
6
|
+
Author: ZarPay
|
|
7
|
+
Author-email: support@zarpay.pk
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: zarpay,payments,jazzcash,easypaisa,pakistan
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: requests>=2.20.0
|
|
17
|
+
Dynamic: author
|
|
18
|
+
Dynamic: author-email
|
|
19
|
+
Dynamic: classifier
|
|
20
|
+
Dynamic: description
|
|
21
|
+
Dynamic: description-content-type
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: keywords
|
|
24
|
+
Dynamic: license
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
Dynamic: requires-dist
|
|
27
|
+
Dynamic: requires-python
|
|
28
|
+
Dynamic: summary
|
|
29
|
+
|
|
30
|
+
# ZarPay Python SDK
|
|
31
|
+
|
|
32
|
+
Official Python SDK for the [ZarPay](https://zarpay.pk) payment gateway.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install zarpay
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from zarpay import ZarPay
|
|
44
|
+
|
|
45
|
+
client = ZarPay("sk_sandbox_xxxxxxxxxxxxx")
|
|
46
|
+
|
|
47
|
+
payment = client.payments.create(
|
|
48
|
+
merchant_order_id="ORD-123",
|
|
49
|
+
amount=1500,
|
|
50
|
+
channel_id=1,
|
|
51
|
+
customer_phone="03001234567",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
print(payment["data"]["status"]) # 'completed' | 'processing' | 'failed'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
### List Available Channels
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
channels = client.channels.list()
|
|
63
|
+
|
|
64
|
+
for ch in channels["data"]["channels"]:
|
|
65
|
+
print(f"{ch['id']}: {ch['wallet_type']}")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Create a Payment
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
payment = client.payments.create(
|
|
72
|
+
merchant_order_id="ORD-456",
|
|
73
|
+
amount=2500,
|
|
74
|
+
channel_id=1,
|
|
75
|
+
customer_phone="03001234567",
|
|
76
|
+
metadata={"customer_name": "Ahmed Khan"},
|
|
77
|
+
idempotency_key="unique-key-456",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if payment["success"]:
|
|
81
|
+
print("Payment completed:", payment["data"]["zarpay_id"])
|
|
82
|
+
else:
|
|
83
|
+
print("Payment failed:", payment["data"]["failure_reason"])
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Get Payment Status
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# By ZarPay ID
|
|
90
|
+
payment = client.payments.get("ZP_abc123def456")
|
|
91
|
+
|
|
92
|
+
# By your order ID
|
|
93
|
+
payment = client.payments.get_by_order_id("ORD-456")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Verify Webhooks
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from zarpay import verify_webhook
|
|
100
|
+
|
|
101
|
+
# In your webhook handler (e.g. Flask/Django)
|
|
102
|
+
def webhook_handler(request):
|
|
103
|
+
try:
|
|
104
|
+
event = verify_webhook(
|
|
105
|
+
raw_body=request.body,
|
|
106
|
+
signature_header=request.headers["X-ZarPay-Signature"],
|
|
107
|
+
secret="whsec_your_webhook_secret",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if event["event"] == "payment.completed":
|
|
111
|
+
# Fulfill the order
|
|
112
|
+
pass
|
|
113
|
+
elif event["event"] == "payment.failed":
|
|
114
|
+
# Notify customer
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
return HttpResponse(status=200)
|
|
118
|
+
except ValueError:
|
|
119
|
+
return HttpResponse(status=400)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Error Handling
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from zarpay import ZarPay, ZarPayAPIError
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
payment = client.payments.create(...)
|
|
129
|
+
except ZarPayAPIError as e:
|
|
130
|
+
print(e.status_code) # 400, 401, 409, etc.
|
|
131
|
+
print(e.error) # Human-readable error message
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Configuration
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
client = ZarPay(
|
|
138
|
+
"sk_sandbox_xxx",
|
|
139
|
+
base_url="http://localhost:3000/api/v1", # local development
|
|
140
|
+
timeout=60, # seconds
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.20.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
zarpay
|