sep-python-sdk 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sep_python_sdk-0.1.0/CHANGELOG.md +24 -0
- sep_python_sdk-0.1.0/CONTRIBUTING.md +21 -0
- sep_python_sdk-0.1.0/LICENSE +21 -0
- sep_python_sdk-0.1.0/MANIFEST.in +4 -0
- sep_python_sdk-0.1.0/PKG-INFO +604 -0
- sep_python_sdk-0.1.0/README.fa.md +569 -0
- sep_python_sdk-0.1.0/README.md +565 -0
- sep_python_sdk-0.1.0/SECURITY.md +19 -0
- sep_python_sdk-0.1.0/examples/django_views.py +57 -0
- sep_python_sdk-0.1.0/examples/fastapi_app.py +89 -0
- sep_python_sdk-0.1.0/examples/flask_app.py +54 -0
- sep_python_sdk-0.1.0/examples/offline_demo.py +41 -0
- sep_python_sdk-0.1.0/pyproject.toml +109 -0
- sep_python_sdk-0.1.0/setup.cfg +4 -0
- sep_python_sdk-0.1.0/src/sep_python_sdk.egg-info/PKG-INFO +604 -0
- sep_python_sdk-0.1.0/src/sep_python_sdk.egg-info/SOURCES.txt +48 -0
- sep_python_sdk-0.1.0/src/sep_python_sdk.egg-info/dependency_links.txt +1 -0
- sep_python_sdk-0.1.0/src/sep_python_sdk.egg-info/requires.txt +10 -0
- sep_python_sdk-0.1.0/src/sep_python_sdk.egg-info/top_level.txt +1 -0
- sep_python_sdk-0.1.0/src/sep_sdk/__init__.py +90 -0
- sep_python_sdk-0.1.0/src/sep_sdk/_base.py +174 -0
- sep_python_sdk-0.1.0/src/sep_sdk/_parsing.py +184 -0
- sep_python_sdk-0.1.0/src/sep_sdk/_version.py +3 -0
- sep_python_sdk-0.1.0/src/sep_sdk/async_client.py +137 -0
- sep_python_sdk-0.1.0/src/sep_sdk/callback.py +52 -0
- sep_python_sdk-0.1.0/src/sep_sdk/client.py +155 -0
- sep_python_sdk-0.1.0/src/sep_sdk/constants.py +43 -0
- sep_python_sdk-0.1.0/src/sep_sdk/endpoints.py +65 -0
- sep_python_sdk-0.1.0/src/sep_sdk/enums.py +173 -0
- sep_python_sdk-0.1.0/src/sep_sdk/errors.py +134 -0
- sep_python_sdk-0.1.0/src/sep_sdk/py.typed +0 -0
- sep_python_sdk-0.1.0/src/sep_sdk/redirect.py +82 -0
- sep_python_sdk-0.1.0/src/sep_sdk/requests.py +194 -0
- sep_python_sdk-0.1.0/src/sep_sdk/responses.py +113 -0
- sep_python_sdk-0.1.0/src/sep_sdk/retry.py +39 -0
- sep_python_sdk-0.1.0/src/sep_sdk/security.py +50 -0
- sep_python_sdk-0.1.0/src/sep_sdk/testing.py +272 -0
- sep_python_sdk-0.1.0/src/sep_sdk/validation.py +99 -0
- sep_python_sdk-0.1.0/tests/__init__.py +0 -0
- sep_python_sdk-0.1.0/tests/conftest.py +33 -0
- sep_python_sdk-0.1.0/tests/helpers.py +66 -0
- sep_python_sdk-0.1.0/tests/test_callback_and_redirect.py +141 -0
- sep_python_sdk-0.1.0/tests/test_client_async.py +142 -0
- sep_python_sdk-0.1.0/tests/test_client_sync.py +519 -0
- sep_python_sdk-0.1.0/tests/test_endpoints_retry.py +47 -0
- sep_python_sdk-0.1.0/tests/test_enums.py +46 -0
- sep_python_sdk-0.1.0/tests/test_fake_and_package.py +115 -0
- sep_python_sdk-0.1.0/tests/test_requests.py +157 -0
- sep_python_sdk-0.1.0/tests/test_security.py +37 -0
- sep_python_sdk-0.1.0/tests/test_validation.py +99 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project follows
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-11-10
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `SepClient` and `AsyncSepClient`: token creation, verify, reverse and the one-call
|
|
14
|
+
`verify_callback` (state check, receipt check, terminal check, amount check, optional
|
|
15
|
+
automatic reversal on a mismatch).
|
|
16
|
+
- `TokenRequest` with every field of the v3.6 guide: wage, cell number, token expiry,
|
|
17
|
+
allowed cards (MD5), four extra-data slots, `TxnRandomSessionKey`, multi-account
|
|
18
|
+
settlement, government payments and `GET` callbacks.
|
|
19
|
+
- `parse_callback`, `render_redirect_form`, `build_send_token_url` and support for terminals
|
|
20
|
+
on `neo-pg` (`X-IPG-Url`).
|
|
21
|
+
- Typed exceptions for every documented result code, `RetryPolicy` with safe-by-default
|
|
22
|
+
retries, and a one-time fallback between the `…Transaction` / `…Transcation` spellings.
|
|
23
|
+
- `sep_sdk.testing.FakeSepGateway`: an in-memory gateway for offline tests.
|
|
24
|
+
- Persian and English documentation.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for helping! This project is small on purpose: every change needs a test, and the
|
|
4
|
+
suite must stay at 100% line and branch coverage.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
python -m venv .venv && source .venv/bin/activate
|
|
8
|
+
pip install -e ".[dev]"
|
|
9
|
+
|
|
10
|
+
ruff format . && ruff check . # style
|
|
11
|
+
mypy # strict typing
|
|
12
|
+
pytest # tests + coverage gate (100%)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Guidelines:
|
|
16
|
+
|
|
17
|
+
- No network in tests. Use `sep_sdk.testing.FakeSepGateway` or `httpx.MockTransport`.
|
|
18
|
+
- Public behaviour changes go in `CHANGELOG.md` under *Unreleased*.
|
|
19
|
+
- Never paste real terminal numbers, tokens, receipts or card data into issues or tests.
|
|
20
|
+
- Match the wording of the vendor's guide for field names; document any guess as a guess.
|
|
21
|
+
- Keep the English and Persian READMEs in sync.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mojtaba Sohrabi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sep-python-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unofficial, fully typed Python SDK for the Saman Electronic Payment (SEP) online payment gateway: token, redirect, callback, verify and reverse.
|
|
5
|
+
Author: Mojtaba Sohrabi
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mojtabasohrabi/sep-python-sdk
|
|
8
|
+
Project-URL: Documentation, https://github.com/mojtabasohrabi/sep-python-sdk#readme
|
|
9
|
+
Project-URL: Source, https://github.com/mojtabasohrabi/sep-python-sdk
|
|
10
|
+
Project-URL: Issues, https://github.com/mojtabasohrabi/sep-python-sdk/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/mojtabasohrabi/sep-python-sdk/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: saman,sep,samanepay,shaparak,payment,payment-gateway,ipg,iran,sdk,httpx,asyncio,typed
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Office/Business :: Financial :: Point-Of-Sale
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: httpx<1,>=0.25
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-cov>=5; extra == "dev"
|
|
34
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
36
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
37
|
+
Requires-Dist: twine>=5; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# sep-python-sdk
|
|
41
|
+
|
|
42
|
+
**English** · [فارسی](README.fa.md)
|
|
43
|
+
|
|
44
|
+
[](https://github.com/mojtabasohrabi/sep-python-sdk/actions/workflows/ci.yml)
|
|
45
|
+
[](https://pypi.org/project/sep-python-sdk/)
|
|
46
|
+
[](https://pypi.org/project/sep-python-sdk/)
|
|
47
|
+
[](LICENSE)
|
|
48
|
+
[](#typing)
|
|
49
|
+
|
|
50
|
+
A typed, tested Python SDK for the **Saman Electronic Payment (SEP) online payment gateway**
|
|
51
|
+
(`sep.shaparak.ir`): request a token, send the customer to the bank, read the callback, verify
|
|
52
|
+
the money and reverse it if you must. Synchronous and `asyncio` clients, no framework required.
|
|
53
|
+
|
|
54
|
+
> **Unofficial.** This project is not affiliated with, endorsed by, or supported by Saman
|
|
55
|
+
> Electronic Payment. It implements the public integration guide (version 3.6). "SEP" and
|
|
56
|
+
> "Saman" belong to their owners.
|
|
57
|
+
|
|
58
|
+
## Contents
|
|
59
|
+
|
|
60
|
+
- [Features](#features)
|
|
61
|
+
- [Install](#install)
|
|
62
|
+
- [How a payment works](#how-a-payment-works)
|
|
63
|
+
- [Quick start](#quick-start)
|
|
64
|
+
- [Guide](#guide)
|
|
65
|
+
- [1. The client](#1-the-client)
|
|
66
|
+
- [2. Create a token](#2-create-a-token)
|
|
67
|
+
- [3. Send the customer to the bank](#3-send-the-customer-to-the-bank)
|
|
68
|
+
- [4. Read the callback](#4-read-the-callback)
|
|
69
|
+
- [5. Verify the payment](#5-verify-the-payment)
|
|
70
|
+
- [6. Reverse a payment](#6-reverse-a-payment)
|
|
71
|
+
- [7. Bind the receipt to the order (session key)](#7-bind-the-receipt-to-the-order-session-key)
|
|
72
|
+
- [8. Multi-account and government payments](#8-multi-account-and-government-payments)
|
|
73
|
+
- [9. Async](#9-async)
|
|
74
|
+
- [10. Errors](#10-errors)
|
|
75
|
+
- [11. Timeouts and retries](#11-timeouts-and-retries)
|
|
76
|
+
- [12. Logging](#12-logging)
|
|
77
|
+
- [13. Testing your integration offline](#13-testing-your-integration-offline)
|
|
78
|
+
- [14. Go-live checklist](#14-go-live-checklist)
|
|
79
|
+
- [Framework examples](#framework-examples)
|
|
80
|
+
- [API reference](#api-reference)
|
|
81
|
+
- [Troubleshooting](#troubleshooting)
|
|
82
|
+
- [Typing](#typing)
|
|
83
|
+
- [Compatibility and versioning](#compatibility-and-versioning)
|
|
84
|
+
- [Contributing](#contributing) · [Security](#security) · [License](#license)
|
|
85
|
+
|
|
86
|
+
## Features
|
|
87
|
+
|
|
88
|
+
- **Complete protocol coverage** — token, redirect (POST form and GET link), callback,
|
|
89
|
+
`VerifyTransaction`, `ReverseTransaction`, multi-account (`MultiSettle`) and government
|
|
90
|
+
(`Government`) payments, allowed-card lists, wage, extra data, token expiry, `GetMethod`,
|
|
91
|
+
`TxnRandomSessionKey`, and `neo-pg` terminals (`X-IPG-Url`).
|
|
92
|
+
- **Safe by construction** — `verify_callback()` refuses unpaid callbacks, checks the receipt,
|
|
93
|
+
the terminal **and the amount**, and can reverse a mismatched payment for you.
|
|
94
|
+
- **Sync and async** — `SepClient` and `AsyncSepClient` share one implementation of every rule.
|
|
95
|
+
- **Strictly typed** — `py.typed`, frozen dataclasses, enums, `mypy --strict` clean.
|
|
96
|
+
- **Honest errors** — one exception hierarchy; every documented `ResultCode` has its own class.
|
|
97
|
+
- **Resilient** — exponential backoff that never repeats a call that could double-charge,
|
|
98
|
+
and a one-time fallback between the two path spellings Saman uses in the wild.
|
|
99
|
+
- **Offline tests included** — `FakeSepGateway` simulates the bank so your tests never need a
|
|
100
|
+
terminal, a whitelisted IP or a network.
|
|
101
|
+
- **Tiny** — one runtime dependency: [`httpx`](https://www.python-httpx.org/). Python 3.10+.
|
|
102
|
+
- **Bilingual** — error and state messages in English and Persian; docs in both languages.
|
|
103
|
+
|
|
104
|
+
## Install
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pip install sep-python-sdk
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
You need a **terminal number** from Saman, and your server's public IP must be registered
|
|
111
|
+
with Saman (token and verify calls are refused otherwise).
|
|
112
|
+
|
|
113
|
+
## How a payment works
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
your server customer's browser Saman (SEP)
|
|
117
|
+
│ 1. create_token(amount, order id, callback URL) ───────────────────────►│
|
|
118
|
+
│◄────────────────────────────────────────────── token ────────────────────│
|
|
119
|
+
│ 2. page with a POST form (render_redirect_form) ─►│ │
|
|
120
|
+
│ │ 3. form POST ─────►│ customer pays
|
|
121
|
+
│ │◄── 4. redirect ─────│
|
|
122
|
+
│◄─ 5. POST/GET to your callback: State, RefNum, ResNum, … ────────────────│
|
|
123
|
+
│ 6. verify_transaction(RefNum) ──────────────────────────────────────────►│
|
|
124
|
+
│◄──────────────────── amount actually taken ───────────────────────────────│
|
|
125
|
+
│ 7. compare with YOUR order total → deliver the goods │
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Two rules the whole SDK is built around:
|
|
129
|
+
|
|
130
|
+
1. **The callback is not proof.** Anyone can send your callback URL any parameters. Only the
|
|
131
|
+
server-to-server verify call proves the money is there.
|
|
132
|
+
2. **Verify within 30 minutes**, or Saman reverses the payment by itself.
|
|
133
|
+
|
|
134
|
+
## Quick start
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from sep_sdk import SepClient, TokenRequest, parse_callback, render_redirect_form
|
|
138
|
+
|
|
139
|
+
sep = SepClient(terminal_id="12345678")
|
|
140
|
+
|
|
141
|
+
# 1. Start: one token per payment attempt. Amounts are in RIALS.
|
|
142
|
+
token = sep.create_token(
|
|
143
|
+
TokenRequest(
|
|
144
|
+
amount=150_000,
|
|
145
|
+
res_num="order-1001", # your unique order / attempt id
|
|
146
|
+
redirect_url="https://shop.example/payments/callback",
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# 2. Send the customer to the bank: return this HTML from a page on your own site.
|
|
151
|
+
html = render_redirect_form(token, language="fa")
|
|
152
|
+
|
|
153
|
+
# 3. In your callback view, the bank posts the result. Parse it, then VERIFY it.
|
|
154
|
+
callback = parse_callback(request_form_or_query) # a dict-like object
|
|
155
|
+
result = sep.verify_callback(callback, expected_amount=150_000) # raises unless truly paid
|
|
156
|
+
|
|
157
|
+
# 4. Only now: mark the order paid and store result.ref_num (it must be unique).
|
|
158
|
+
print(result.ref_num, result.amount)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Try the whole flow offline, without a terminal: `python examples/offline_demo.py`.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Guide
|
|
166
|
+
|
|
167
|
+
### 1. The client
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from sep_sdk import SepClient
|
|
171
|
+
|
|
172
|
+
with SepClient(terminal_id="12345678") as sep: # closes the connection pool on exit
|
|
173
|
+
...
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
| Argument | Default | Meaning |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| `terminal_id` | – | Your numeric terminal number (`str` or `int`). |
|
|
179
|
+
| `endpoints` | production | An [`Endpoints`](#endpoints) object, to override or fake any URL. |
|
|
180
|
+
| `timeout` | 20 s (5 s connect) | A `float` or `httpx.Timeout`. |
|
|
181
|
+
| `retry` | 3 attempts | A [`RetryPolicy`](#11-timeouts-and-retries). |
|
|
182
|
+
| `endpoint_fallback` | `True` | Try the alternative path spelling once on HTTP 404. |
|
|
183
|
+
| `transport` | – | An `httpx` transport (tests, proxies). |
|
|
184
|
+
| `http_client` | – | Your own `httpx.Client`; you then close it. |
|
|
185
|
+
|
|
186
|
+
Create **one client per process** and reuse it; it keeps connections alive.
|
|
187
|
+
|
|
188
|
+
#### Endpoints
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from sep_sdk import Endpoints, SepClient
|
|
192
|
+
|
|
193
|
+
SepClient("12345678", endpoints=Endpoints.production()) # the default
|
|
194
|
+
SepClient("12345678", endpoints=Endpoints.legacy()) # "…Transcation" first
|
|
195
|
+
SepClient("12345678", endpoints=Endpoints.without_fallback())
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### 2. Create a token
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from sep_sdk import TokenRequest
|
|
202
|
+
|
|
203
|
+
token = sep.create_token(
|
|
204
|
+
TokenRequest(
|
|
205
|
+
amount=150_000, # rials, integer, > 0
|
|
206
|
+
res_num="order-1001", # unique per attempt
|
|
207
|
+
redirect_url="https://shop.example/payments/callback",
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
token.token # the payment token (hidden from repr())
|
|
211
|
+
token.expires_in_minutes # 20 unless you changed it
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
All `TokenRequest` fields (everything but the first three is optional). Invalid values raise
|
|
215
|
+
`SepValidationError` **before** anything is sent.
|
|
216
|
+
|
|
217
|
+
| Field | Guide name | Notes |
|
|
218
|
+
|---|---|---|
|
|
219
|
+
| `amount` | `Amount` | Rials, integer. |
|
|
220
|
+
| `res_num` | `ResNum` | Your order / attempt id. Make it unique: it is your only handle if the customer paid but your server never heard back. |
|
|
221
|
+
| `redirect_url` | `RedirectUrl` | Your callback. `http(s)`, max 2083 characters (1538 with `callback_method="GET"`). |
|
|
222
|
+
| `wage` | `Wage` | Fee, for merchants who split payments. |
|
|
223
|
+
| `cell_number` | `CellNumber` | Customer mobile, so Saman can offer saved cards. Accepts `0912…`, `+98912…`, `912…`, Persian digits. |
|
|
224
|
+
| `token_expiry_minutes` | `TokenExpiryInMin` | 20 – 3600. |
|
|
225
|
+
| `allowed_cards` | `HashedCardNumber` | Up to 10 MD5 hashes; only those cards may pay. Use `hash_card_number()`. |
|
|
226
|
+
| `extra_data` | `ResNum1…4` | Up to 4 strings of ≤ 50 chars, visible only in Saman's reports. |
|
|
227
|
+
| `session_key` | `TxnRandomSessionKey` | See [section 7](#7-bind-the-receipt-to-the-order-session-key). |
|
|
228
|
+
| `settlement` | `SettlementIbanInfo` | See [section 8](#8-multi-account-and-government-payments). |
|
|
229
|
+
| `government` | `TranType=Government` | See section 8. |
|
|
230
|
+
| `callback_method` | `GetMethod` | `"POST"` (default) or `"GET"`. |
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from sep_sdk import hash_card_number
|
|
234
|
+
|
|
235
|
+
TokenRequest(
|
|
236
|
+
amount=150_000, res_num="order-1002", redirect_url="https://shop.example/cb",
|
|
237
|
+
cell_number="09123456789",
|
|
238
|
+
token_expiry_minutes=60,
|
|
239
|
+
allowed_cards=[hash_card_number("6037 9912 3456 7890")], # only this card may pay
|
|
240
|
+
extra_data=["invoice 2026-17", "campaign spring"],
|
|
241
|
+
)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
> `hash_card_number` never stores or logs the number. The guide forbids storing card numbers:
|
|
245
|
+
> keep only the hash.
|
|
246
|
+
|
|
247
|
+
### 3. Send the customer to the bank
|
|
248
|
+
|
|
249
|
+
The customer's **browser** must go to Saman. Use the form, which works for every terminal:
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
from sep_sdk import render_redirect_form
|
|
253
|
+
|
|
254
|
+
html = render_redirect_form(token, language="fa", nonce=csp_nonce_or_None)
|
|
255
|
+
# return it as an HTML response from a page on YOUR site
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
It renders a page that POSTs `Token` to the bank at once (and shows a button if JavaScript is
|
|
259
|
+
off). Every value is HTML-escaped; pass `nonce=` if your Content-Security-Policy needs one.
|
|
260
|
+
|
|
261
|
+
**Important:** Saman refuses visitors who arrive without a `Referer` from your site. Serve the
|
|
262
|
+
form from your own domain, don't send `Referrer-Policy: no-referrer` on that page (the form
|
|
263
|
+
already includes `<meta name="referrer" content="origin">`), and don't open the bank URL from a
|
|
264
|
+
mobile app directly: open your own page in a browser instead.
|
|
265
|
+
|
|
266
|
+
Prefer to template it yourself?
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
from sep_sdk import redirect_form_fields
|
|
270
|
+
|
|
271
|
+
redirect_form_fields(token) # {"Token": "…"} (+ "GetMethod": "true" for GET callbacks)
|
|
272
|
+
token.form_action # where to POST it
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
A plain link also exists for classic terminals: `token.get_url`
|
|
276
|
+
(`https://sep.shaparak.ir/OnlinePG/SendToken?token=…`). Terminals moved to **neo-pg** return an
|
|
277
|
+
`X-IPG-Url` header; then `token.ipg_url` is set, `token.get_url` is `None`, and the form's
|
|
278
|
+
`action` already points at the new address.
|
|
279
|
+
|
|
280
|
+
### 4. Read the callback
|
|
281
|
+
|
|
282
|
+
After the payment Saman redirects the customer to your `redirect_url` carrying the result
|
|
283
|
+
(`POST` form fields, or a query string with `callback_method="GET"`).
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
from sep_sdk import parse_callback
|
|
287
|
+
|
|
288
|
+
callback = parse_callback(request.POST) # Django QueryDict, Flask request.values,
|
|
289
|
+
# a plain dict, parse_qs(...) output…
|
|
290
|
+
callback.res_num # your order id (ResNum)
|
|
291
|
+
callback.ref_num # the bank's receipt (RefNum)
|
|
292
|
+
callback.state # TransactionState.OK, CANCELED_BY_USER, FAILED, …
|
|
293
|
+
callback.is_ok # State == OK, a receipt exists, and Status (if sent) agrees
|
|
294
|
+
callback.amount # what the bank *says* it took: NOT trustworthy
|
|
295
|
+
callback.state.message("fa") # a customer-friendly explanation (also "en")
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
`parse_callback` makes **no network call** and trusts nothing. Names are matched
|
|
299
|
+
case-insensitively; list values are flattened; the card-related fields are hidden from `repr`.
|
|
300
|
+
|
|
301
|
+
| `TransactionState` | Meaning |
|
|
302
|
+
|---|---|
|
|
303
|
+
| `OK` | The bank reports a completed payment (still: verify!). |
|
|
304
|
+
| `CANCELED_BY_USER` | The customer cancelled. |
|
|
305
|
+
| `FAILED` | Payment failed. |
|
|
306
|
+
| `SESSION_IS_NULL` | The customer did not respond in time. |
|
|
307
|
+
| `INVALID_PARAMETERS` | Parameters sent to the bank were invalid. |
|
|
308
|
+
| `MERCHANT_IP_ADDRESS_IS_INVALID` | Your server IP is not registered with Saman. |
|
|
309
|
+
| `TOKEN_NOT_FOUND` / `TOKEN_REQUIRED` | Unknown token / terminal accepts token payments only. |
|
|
310
|
+
| `TERMINAL_NOT_FOUND` | Unknown terminal number. |
|
|
311
|
+
| `MULTISETTLE_POLICY_ERRORS` | Multi-account rules were violated. |
|
|
312
|
+
| `UNKNOWN` | A value this SDK version does not know — treated as *not paid*. |
|
|
313
|
+
|
|
314
|
+
### 5. Verify the payment
|
|
315
|
+
|
|
316
|
+
The one call you should use:
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
from sep_sdk import AmountMismatchError, PaymentNotCompletedError, SepError
|
|
320
|
+
|
|
321
|
+
order = orders.get(callback.res_num) # YOUR record, found by your order id
|
|
322
|
+
try:
|
|
323
|
+
result = sep.verify_callback(
|
|
324
|
+
callback,
|
|
325
|
+
expected_amount=order.total_rials, # from your database, NEVER from the request
|
|
326
|
+
session_key=order.session_key, # if you used one
|
|
327
|
+
reverse_on_mismatch=True, # refund automatically if the amount differs
|
|
328
|
+
)
|
|
329
|
+
except PaymentNotCompletedError as error: # cancelled / failed: nothing to verify
|
|
330
|
+
show(error.state.message("fa"))
|
|
331
|
+
except AmountMismatchError as error: # the bank took another sum
|
|
332
|
+
log.error("tampered payment: %s vs %s, reversed=%s", error.expected, error.actual, error.reversed)
|
|
333
|
+
except SepError: # network, gateway refusal, bad answer…
|
|
334
|
+
schedule_retry_within_30_minutes(callback.ref_num)
|
|
335
|
+
else:
|
|
336
|
+
# Receipts may be used ONCE. Store ref_num in a UNIQUE column; a duplicate = replay.
|
|
337
|
+
order.mark_paid(receipt=result.ref_num)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
`verify_callback` does, in order:
|
|
341
|
+
|
|
342
|
+
1. refuses a callback whose `State` isn't a clean `OK` (`PaymentNotCompletedError`, no network);
|
|
343
|
+
2. refuses a callback for another terminal;
|
|
344
|
+
3. calls `VerifyTransaction`;
|
|
345
|
+
4. checks the answer is for the **same receipt and terminal**;
|
|
346
|
+
5. checks the bank took exactly `expected_amount` rials (`AmountMismatchError`; with
|
|
347
|
+
`reverse_on_mismatch=True` it also reverses, and `error.reversed` tells you the outcome).
|
|
348
|
+
|
|
349
|
+
Need finer control? Call the primitive yourself:
|
|
350
|
+
|
|
351
|
+
```python
|
|
352
|
+
result = sep.verify_transaction(callback.ref_num, session_key=order.session_key)
|
|
353
|
+
result.amount # OrginalAmount: compare with YOUR total
|
|
354
|
+
result.detail.affective_amount # what left the card (differs on discount terminals)
|
|
355
|
+
result.detail.trace_no, result.detail.rrn, result.detail.trace_date
|
|
356
|
+
result.is_duplicate # True when Saman says "this request was already processed"
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
If the verify call fails because of the network, **retry**: Saman keeps the receipt valid and
|
|
360
|
+
answers a repeat with "duplicate request". The SDK already retries (see section 11); if it still
|
|
361
|
+
fails, try again within the 30-minute window — a background job that re-verifies unfinished
|
|
362
|
+
payments is a good idea.
|
|
363
|
+
|
|
364
|
+
### 6. Reverse a payment
|
|
365
|
+
|
|
366
|
+
```python
|
|
367
|
+
reversal = sep.reverse_transaction(ref_num) # up to 50 minutes after the payment
|
|
368
|
+
reversal.is_duplicate # True if it was already reversed
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### 7. Bind the receipt to the order (session key)
|
|
372
|
+
|
|
373
|
+
`TxnRandomSessionKey` ties the later verify call to the exact payment, so a receipt from one
|
|
374
|
+
(cheap) order cannot be replayed on another. Two ways to get a key:
|
|
375
|
+
|
|
376
|
+
```python
|
|
377
|
+
from sep_sdk import derive_session_key, generate_session_key
|
|
378
|
+
|
|
379
|
+
key = generate_session_key() # random; store it with the order
|
|
380
|
+
key = derive_session_key(SERVER_SECRET, order_id) # or recompute it later, nothing to store
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Pass it as `TokenRequest(session_key=key)` and again as
|
|
384
|
+
`verify_callback(..., session_key=key)`. If Saman rejects the extra field for your terminal,
|
|
385
|
+
simply don't send it. (The in-memory fake treats a missing or wrong key as "transaction not
|
|
386
|
+
found"; the real gateway's wording is not in the guide.)
|
|
387
|
+
|
|
388
|
+
### 8. Multi-account and government payments
|
|
389
|
+
|
|
390
|
+
Needs a terminal enabled for it by Saman.
|
|
391
|
+
|
|
392
|
+
```python
|
|
393
|
+
from sep_sdk import SettlementItem, TokenRequest
|
|
394
|
+
|
|
395
|
+
# Split one payment between several IBANs (at most 9 lines).
|
|
396
|
+
TokenRequest(
|
|
397
|
+
amount=12_000, res_num="order-7", redirect_url="https://shop.example/cb",
|
|
398
|
+
settlement=[
|
|
399
|
+
SettlementItem(iban="IR062960000000100324200001", amount=7_000),
|
|
400
|
+
SettlementItem(iban="IR000000000000000000000001", amount=5_000),
|
|
401
|
+
],
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Government payment: the FIRST line carries the government IBAN and its 30-digit id.
|
|
405
|
+
TokenRequest(
|
|
406
|
+
amount=12_000, res_num="order-8", redirect_url="https://shop.example/cb",
|
|
407
|
+
government=True,
|
|
408
|
+
settlement=[
|
|
409
|
+
SettlementItem(iban="IR062960000000100324200001", amount=7_000, purchase_id="1" * 30),
|
|
410
|
+
SettlementItem(iban="IR000000000000000000000001", amount=5_000), # padded with 30 zeros
|
|
411
|
+
],
|
|
412
|
+
)
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
IBANs are checked for shape (`IR` + 24 digits); `iban_checksum_ok(iban)` is available if you
|
|
416
|
+
also want the mod-97 check.
|
|
417
|
+
|
|
418
|
+
### 9. Async
|
|
419
|
+
|
|
420
|
+
`AsyncSepClient` has the same methods, awaited:
|
|
421
|
+
|
|
422
|
+
```python
|
|
423
|
+
from sep_sdk import AsyncSepClient
|
|
424
|
+
|
|
425
|
+
async with AsyncSepClient(terminal_id="12345678") as sep:
|
|
426
|
+
token = await sep.create_token(request)
|
|
427
|
+
result = await sep.verify_callback(callback, expected_amount=150_000)
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### 10. Errors
|
|
431
|
+
|
|
432
|
+
Everything derives from `SepError`.
|
|
433
|
+
|
|
434
|
+
```
|
|
435
|
+
SepError
|
|
436
|
+
├── SepConfigurationError bad terminal id / endpoints / retry policy (also ValueError)
|
|
437
|
+
├── SepValidationError request rejected locally, nothing was sent (also ValueError)
|
|
438
|
+
├── SepTransportError the outcome is unknown
|
|
439
|
+
│ ├── SepConnectionError
|
|
440
|
+
│ └── SepTimeoutError
|
|
441
|
+
├── SepHTTPError unexpected HTTP status (.status_code)
|
|
442
|
+
├── SepResponseError 2xx but not what the guide describes
|
|
443
|
+
├── SepAPIError the gateway refused (.code, .description, .operation)
|
|
444
|
+
│ ├── SepTokenError token request refused (status = -1)
|
|
445
|
+
│ └── SepTransactionError verify/reverse refused (.result_code)
|
|
446
|
+
│ ├── TransactionNotFoundError -2
|
|
447
|
+
│ ├── VerifyWindowExpiredError -6 (more than 30 minutes)
|
|
448
|
+
│ ├── TransactionReversedError 5
|
|
449
|
+
│ ├── TerminalError -104 / -105
|
|
450
|
+
│ └── IPNotAllowedError -106
|
|
451
|
+
├── PaymentNotCompletedError callback says the customer did not pay (.state)
|
|
452
|
+
└── AmountMismatchError the bank took another sum (.expected, .actual, .reversed)
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
`description` is text written by the gateway: log it, don't show it to customers verbatim.
|
|
456
|
+
For customer-facing text use `state.message("fa" | "en")` and `ResultCode.message(...)`.
|
|
457
|
+
|
|
458
|
+
### 11. Timeouts and retries
|
|
459
|
+
|
|
460
|
+
```python
|
|
461
|
+
import httpx
|
|
462
|
+
from sep_sdk import RetryPolicy, SepClient
|
|
463
|
+
|
|
464
|
+
SepClient(
|
|
465
|
+
"12345678",
|
|
466
|
+
timeout=httpx.Timeout(15.0, connect=3.0),
|
|
467
|
+
retry=RetryPolicy(max_attempts=4, backoff_factor=0.5, max_backoff=8.0),
|
|
468
|
+
)
|
|
469
|
+
SepClient("12345678", retry=RetryPolicy.none()) # exactly one attempt
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
| Operation | Connection failed to open | Timeout / reset / 5xx / 429 |
|
|
473
|
+
|---|---|---|
|
|
474
|
+
| `verify`, `reverse` | retried | retried (Saman answers a repeat with "duplicate request") |
|
|
475
|
+
| `create_token` | retried | **not retried** — the bank may already have created a session |
|
|
476
|
+
|
|
477
|
+
HTTP 404 on verify/reverse triggers a one-time switch to the alternative path spelling
|
|
478
|
+
(`VerifyTransaction` ↔ `VerifyTranscation`), remembered for the client's lifetime.
|
|
479
|
+
|
|
480
|
+
### 12. Logging
|
|
481
|
+
|
|
482
|
+
The SDK logs under the `sep_sdk` logger (silent unless you configure it): debug for attempts,
|
|
483
|
+
warnings for retries and fallbacks, an error if an automatic reversal fails. Tokens, receipts,
|
|
484
|
+
card data and request bodies are **never** logged.
|
|
485
|
+
|
|
486
|
+
```python
|
|
487
|
+
import logging
|
|
488
|
+
logging.getLogger("sep_sdk").setLevel(logging.INFO)
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### 13. Testing your integration offline
|
|
492
|
+
|
|
493
|
+
`FakeSepGateway` is an in-memory bank:
|
|
494
|
+
|
|
495
|
+
```python
|
|
496
|
+
from sep_sdk import SepClient, TokenRequest, parse_callback
|
|
497
|
+
from sep_sdk.testing import FakeSepGateway
|
|
498
|
+
|
|
499
|
+
fake = FakeSepGateway(terminal_id="12345678")
|
|
500
|
+
sep = SepClient("12345678", endpoints=fake.endpoints, transport=fake.transport)
|
|
501
|
+
|
|
502
|
+
token = sep.create_token(TokenRequest(amount=100_000, res_num="o-1", redirect_url="https://x.test/cb"))
|
|
503
|
+
|
|
504
|
+
fields = fake.complete_payment(token.token) # what the bank would POST to you
|
|
505
|
+
fields = fake.complete_payment(token.token, paid_amount=1_000) # a tampered payment
|
|
506
|
+
fields = fake.end_payment(token.token) # customer cancelled
|
|
507
|
+
fake.expire(ref_num) # 30 minutes passed: verify → -6
|
|
508
|
+
fake.fail_next(status_code=503, operation="verify") # HTTP error on the next verify
|
|
509
|
+
fake.drop_next() # lose the next call (connection error)
|
|
510
|
+
FakeSepGateway(reject_ip=True) # server IP not registered
|
|
511
|
+
FakeSepGateway(neo_pg_url="https://neo-pg.sep.test/transaction/init")
|
|
512
|
+
FakeSepGateway(spelling="transcation") # exercise the endpoint fallback
|
|
513
|
+
fake.calls # every request, for assertions
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
It follows the **guide**, not Saman's private behaviour. Treat it as a way to exercise your own
|
|
517
|
+
code paths, and still run one small real payment before launch. `AsyncSepClient` accepts
|
|
518
|
+
`transport=fake.transport` too.
|
|
519
|
+
|
|
520
|
+
### 14. Go-live checklist
|
|
521
|
+
|
|
522
|
+
- [ ] Your server's public IP is registered with Saman.
|
|
523
|
+
- [ ] `expected_amount` comes from your database; the callback's `Amount` is never used.
|
|
524
|
+
- [ ] Receipts (`RefNum`) are stored under a unique constraint; a repeat is a replay.
|
|
525
|
+
- [ ] Verification also runs from a background job for payments whose callback never arrived
|
|
526
|
+
(look up pending orders by `res_num` and ask the customer/Saman's report panel if needed).
|
|
527
|
+
- [ ] The callback endpoint is reachable by the customer's browser, accepts both `POST` and
|
|
528
|
+
(if you use `GET`) `GET`, is exempt from CSRF protection, and answers quickly.
|
|
529
|
+
- [ ] The redirect page is served from your domain without `Referrer-Policy: no-referrer`.
|
|
530
|
+
- [ ] A real payment of a small amount worked end to end, including a cancelled one.
|
|
531
|
+
- [ ] Logs contain no tokens, receipts or card data.
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## Framework examples
|
|
536
|
+
|
|
537
|
+
Runnable-in-spirit, complete examples live in [`examples/`](examples):
|
|
538
|
+
|
|
539
|
+
- [`offline_demo.py`](examples/offline_demo.py) — the whole flow with the fake gateway (runs as is).
|
|
540
|
+
- [`fastapi_app.py`](examples/fastapi_app.py) — async, with `lifespan`.
|
|
541
|
+
- [`flask_app.py`](examples/flask_app.py) — synchronous.
|
|
542
|
+
- [`django_views.py`](examples/django_views.py) — with a unique receipt column.
|
|
543
|
+
|
|
544
|
+
## API reference
|
|
545
|
+
|
|
546
|
+
**Clients** — `SepClient`, `AsyncSepClient`
|
|
547
|
+
|
|
548
|
+
| Method | Returns | Purpose |
|
|
549
|
+
|---|---|---|
|
|
550
|
+
| `create_token(request)` | `Token` | Start a payment. |
|
|
551
|
+
| `verify_transaction(ref_num, *, session_key=None)` | `TransactionResult` | Confirm a receipt. |
|
|
552
|
+
| `reverse_transaction(ref_num, *, session_key=None)` | `TransactionResult` | Refund. |
|
|
553
|
+
| `verify_callback(callback, *, expected_amount, session_key=None, reverse_on_mismatch=False)` | `TransactionResult` | The safe, complete check. |
|
|
554
|
+
| `render_redirect_form(token, *, language, nonce)` | `str` | HTML hand-off page. |
|
|
555
|
+
| `close()` / `aclose()` | – | Release connections. |
|
|
556
|
+
|
|
557
|
+
**Functions** — `parse_callback`, `render_redirect_form`, `redirect_form_fields`,
|
|
558
|
+
`build_send_token_url`, `hash_card_number`, `generate_session_key`, `derive_session_key`,
|
|
559
|
+
`iban_checksum_ok`.
|
|
560
|
+
|
|
561
|
+
**Data** — `TokenRequest`, `SettlementItem`, `Token`, `CallbackData`, `TransactionResult`,
|
|
562
|
+
`TransactionDetail`, `Endpoints`, `RetryPolicy`.
|
|
563
|
+
|
|
564
|
+
**Enums** — `TransactionState`, `TransactionStatus`, `ResultCode`.
|
|
565
|
+
|
|
566
|
+
**Testing** — `sep_sdk.testing.FakeSepGateway`.
|
|
567
|
+
|
|
568
|
+
## Troubleshooting
|
|
569
|
+
|
|
570
|
+
| Symptom | Likely cause |
|
|
571
|
+
|---|---|
|
|
572
|
+
| `SepTokenError` with `code=8`, or callback `MerchantIpAddressIsInvalid` | Your server's IP is not registered with Saman. |
|
|
573
|
+
| `IPNotAllowedError` (`-106`) on verify | Same, for the verify service. |
|
|
574
|
+
| The bank page refuses to open | No `Referer`: serve the form from your own site (see section 3). |
|
|
575
|
+
| `TransactionNotFoundError` right after a real payment | Wrong `session_key`, or the receipt is not the one Saman issued. |
|
|
576
|
+
| `VerifyWindowExpiredError` | More than 30 minutes passed; Saman has reversed the payment. |
|
|
577
|
+
| `SepHTTPError` with status 404 | Endpoint path changed. Override `Endpoints`. |
|
|
578
|
+
| `AmountMismatchError` | The bank took another sum. Never deliver; refund (`reverse_on_mismatch=True`). |
|
|
579
|
+
|
|
580
|
+
## Typing
|
|
581
|
+
|
|
582
|
+
The package ships `py.typed` and is checked with `mypy --strict`. Models are frozen,
|
|
583
|
+
slotted dataclasses; state and codes are enums; unions are explicit (`ResultCode | int`
|
|
584
|
+
where the gateway may send a code this version does not know).
|
|
585
|
+
|
|
586
|
+
## Compatibility and versioning
|
|
587
|
+
|
|
588
|
+
Python 3.10 – 3.13. [Semantic Versioning](https://semver.org/). Built from Saman's integration
|
|
589
|
+
guide v3.6; a few details are not fully specified there (the exact `TxnRandomSessionKey`
|
|
590
|
+
semantics, the `…Transaction` vs `…Transcation` path spelling, `neo-pg` GET redirects) and are
|
|
591
|
+
handled defensively — please open an issue if your terminal behaves differently.
|
|
592
|
+
|
|
593
|
+
## Contributing
|
|
594
|
+
|
|
595
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). The suite runs offline with 100% line and branch
|
|
596
|
+
coverage; `ruff`, `mypy --strict` and `pytest` must pass.
|
|
597
|
+
|
|
598
|
+
## Security
|
|
599
|
+
|
|
600
|
+
Report vulnerabilities privately — see [SECURITY.md](SECURITY.md).
|
|
601
|
+
|
|
602
|
+
## License
|
|
603
|
+
|
|
604
|
+
[MIT](LICENSE).
|