payshen 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.
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
@@ -0,0 +1,25 @@
1
+ name: CI
2
+
3
+ # Public repository: GitHub-hosted runners are free here. Still started by
4
+ # hand or by a pull request only, never by every push.
5
+ on:
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: actions/setup-python@v5
22
+ with:
23
+ python-version: ${{ matrix.python }}
24
+ - run: python -m pip install -e ".[dev]"
25
+ - run: python -m pytest -q
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ /dist/
4
+ /build/
5
+ *.egg-info/
6
+ .pytest_cache/
7
+ .venv/
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ The first release, with the same rules as the TypeScript SDK (`@payshen/sdk` 0.9.3).
6
+
7
+ - Payments: create, get, list, `all()` over every page, events, capture (full or partial), void, refund (full or
8
+ partial).
9
+ - Events feed, insights, suggestions (list, get, accept, reject, snooze), partner merchants.
10
+ - An `Idempotency-Key` on every POST, yours or generated and reused across retries.
11
+ - Retries on 408, 425, 429, 500, 502, 503 and 504 with backoff and jitter, never on an outcome the API says not to
12
+ retry (`payment_result_unknown`, `*_result_unknown`, `state_unconfirmed`, `state_conflict`, `psp_error`,
13
+ `provider_unavailable`, `merchant_limit`). `Retry-After` is honoured, capped at 4 seconds.
14
+ - Webhooks: `verify_webhook_signature`, `parse_webhook_event`, `claim_webhook_event` with a store, and secret
15
+ rotation.
16
+ - HTTPS only, except localhost; redirects are never followed. No dependencies.
payshen-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Payshen
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.
payshen-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.5
2
+ Name: payshen
3
+ Version: 0.1.0
4
+ Summary: Python client for the Payshen payment orchestration API: payments, captures, voids, refunds, events and webhook verification.
5
+ Project-URL: Homepage, https://www.payshen.com/developers
6
+ Project-URL: Documentation, https://www.payshen.com/developers
7
+ Project-URL: API reference, https://www.payshen.com/api/v1/openapi.json
8
+ Author: Sodasoft LLC
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: payment orchestration,payments,payshen,psp,webhooks
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Topic :: Office/Business :: Financial
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.9
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=8; extra == 'dev'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Payshen Python
24
+
25
+ The Python client for the [Payshen](https://www.payshen.com) API: create and route payments, capture, void and
26
+ refund them, read their timeline, and verify the webhooks Payshen sends.
27
+
28
+ Payshen routes each payment to one of the providers you already have (Stripe, Adyen, Checkout.com, payabl. and
29
+ more). Payshen never holds or moves money: payments run on your own provider accounts.
30
+
31
+ - Python 3.9 or newer. No dependencies: the standard library only.
32
+ - Every POST carries an `Idempotency-Key`, so a retry after a dropped connection never charges twice.
33
+ - Retries with backoff and jitter, on the answers worth retrying and never on the ones the API says not to.
34
+ - Typed exceptions with the API's own error code and the request id support will ask for.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install payshen
40
+ ```
41
+
42
+ ## Create a payment
43
+
44
+ ```python
45
+ import os
46
+ from payshen import Payshen
47
+
48
+ payshen = Payshen(os.environ["PAYSHEN_API_KEY"]) # ps_test_... in the sandbox
49
+
50
+ payment = payshen.payments.create(
51
+ {
52
+ "amount": 5000, # minor units: EUR 50.00
53
+ "currency": "EUR",
54
+ "country": "DE",
55
+ "reference": "order-1024",
56
+ "capture_method": "manual", # hold now, capture when you ship
57
+ "return_url": "https://yourshop.com/checkout/return",
58
+ },
59
+ idempotency_key="order-1024",
60
+ )
61
+
62
+ # REQUIRES_ACTION: send the customer to the provider's page.
63
+ redirect_to = payment["next_action"]["url"]
64
+ ```
65
+
66
+ The customer pays on the provider's page and comes back to your `return_url`. The return is not the outcome: the
67
+ outcome is the webhook (`payment.authorized`, `payment.captured`, `payment.declined`, ...).
68
+
69
+ ## Capture, void, refund
70
+
71
+ ```python
72
+ from payshen import PayshenError
73
+
74
+ payshen.payments.capture(payment_id, idempotency_key="capture-order-1024") # in full
75
+ payshen.payments.capture(payment_id, amount=3000, idempotency_key="capture-order-1024") # or in part
76
+ payshen.payments.void(payment_id, idempotency_key="void-order-1024") # before capture
77
+
78
+ try:
79
+ payshen.payments.refund(payment_id, amount=2000, idempotency_key="refund-order-1024-1")
80
+ except PayshenError as e:
81
+ # 409 refund_pending: accepted, not failed. The provider confirms a moment later and
82
+ # payment.refunded or payment.refund_failed follows. Do not send it again under a new key.
83
+ if e.code != "refund_pending":
84
+ raise
85
+ ```
86
+
87
+ A capture or void a provider only accepted answers with the payment `PENDING`; it becomes `CAPTURED` or
88
+ `CANCELLED` when the provider confirms, and the webhook follows.
89
+
90
+ ## Read
91
+
92
+ ```python
93
+ payment = payshen.payments.get(payment_id)
94
+ timeline = payshen.payments.events(payment_id)
95
+
96
+ page = payshen.payments.list(limit=50, status="captured")
97
+ for payment in payshen.payments.all(status="captured"):
98
+ ... # every page, newest first
99
+
100
+ for event in payshen.events.all(type="payment.captured"):
101
+ ... # the same events webhooks deliver, for polling
102
+ ```
103
+
104
+ ## Webhooks
105
+
106
+ ```python
107
+ from payshen import SignatureVerificationError, parse_webhook_event
108
+
109
+ # Flask. Django: request.body and request.headers["X-Payshen-Signature"].
110
+ raw = request.get_data() # the RAW body: a body re-encoded from json never verifies
111
+ try:
112
+ event = parse_webhook_event(raw, request.headers.get("X-Payshen-Signature"), os.environ["PAYSHEN_WEBHOOK_SECRET"])
113
+ except SignatureVerificationError:
114
+ return "", 400
115
+
116
+ # Deliveries are retried, so the same event can arrive twice. Record event["id"]
117
+ # and skip one you have already processed.
118
+ if event["type"] == "payment.captured":
119
+ fulfil(event["data"]["reference"])
120
+ return "", 204
121
+ ```
122
+
123
+ The signature proves a delivery came from Payshen and is recent (5 minutes by default); it does not make it new.
124
+ `claim_webhook_event(raw, header, secret, store)` verifies, parses and takes each event once, with a store you back
125
+ with your database:
126
+
127
+ ```python
128
+ class DbEventStore:
129
+ def claim(self, event_id: str) -> bool:
130
+ # PostgreSQL; MySQL: INSERT IGNORE and the affected row count
131
+ cur.execute("INSERT INTO processed_webhook_events (id) VALUES (%s) ON CONFLICT (id) DO NOTHING RETURNING id", (event_id,))
132
+ return cur.fetchone() is not None
133
+ ```
134
+
135
+ During a secret rotation pass both: `parse_webhook_event(raw, header, [new_secret, old_secret])`.
136
+
137
+ ## Errors
138
+
139
+ | Exception | When |
140
+ |---|---|
141
+ | `AuthenticationError` | 401 or 403: a missing, wrong or revoked key, or one not allowed this call |
142
+ | `ValidationError` | 422: refused as sent; `code` names the rule |
143
+ | `RateLimitError` | 429 after the client's retries; `retry_after_seconds` when the API said |
144
+ | `NetworkError` | No answer at all. On a POST, retry with the same idempotency key |
145
+ | `PayshenError` | Everything else, and the parent of all of the above |
146
+
147
+ Every exception has `status`, `code` (`refund_pending`, `not_capturable`, `payment_result_unknown`, ...) and
148
+ `request_id`. Branch on `code`, not on the message.
149
+
150
+ ## Options
151
+
152
+ ```python
153
+ Payshen(key, timeout=20.0, max_retries=3, base_url="https://www.payshen.com")
154
+ ```
155
+
156
+ `payshen.request("GET", "/api/v1/...")` calls any endpoint and returns the whole answer.
157
+
158
+ ## Documentation
159
+
160
+ - API reference and quickstart: <https://www.payshen.com/developers>
161
+ - OpenAPI 3.1: <https://www.payshen.com/api/v1/openapi.json>
162
+
163
+ ## License
164
+
165
+ MIT
@@ -0,0 +1,143 @@
1
+ # Payshen Python
2
+
3
+ The Python client for the [Payshen](https://www.payshen.com) API: create and route payments, capture, void and
4
+ refund them, read their timeline, and verify the webhooks Payshen sends.
5
+
6
+ Payshen routes each payment to one of the providers you already have (Stripe, Adyen, Checkout.com, payabl. and
7
+ more). Payshen never holds or moves money: payments run on your own provider accounts.
8
+
9
+ - Python 3.9 or newer. No dependencies: the standard library only.
10
+ - Every POST carries an `Idempotency-Key`, so a retry after a dropped connection never charges twice.
11
+ - Retries with backoff and jitter, on the answers worth retrying and never on the ones the API says not to.
12
+ - Typed exceptions with the API's own error code and the request id support will ask for.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install payshen
18
+ ```
19
+
20
+ ## Create a payment
21
+
22
+ ```python
23
+ import os
24
+ from payshen import Payshen
25
+
26
+ payshen = Payshen(os.environ["PAYSHEN_API_KEY"]) # ps_test_... in the sandbox
27
+
28
+ payment = payshen.payments.create(
29
+ {
30
+ "amount": 5000, # minor units: EUR 50.00
31
+ "currency": "EUR",
32
+ "country": "DE",
33
+ "reference": "order-1024",
34
+ "capture_method": "manual", # hold now, capture when you ship
35
+ "return_url": "https://yourshop.com/checkout/return",
36
+ },
37
+ idempotency_key="order-1024",
38
+ )
39
+
40
+ # REQUIRES_ACTION: send the customer to the provider's page.
41
+ redirect_to = payment["next_action"]["url"]
42
+ ```
43
+
44
+ The customer pays on the provider's page and comes back to your `return_url`. The return is not the outcome: the
45
+ outcome is the webhook (`payment.authorized`, `payment.captured`, `payment.declined`, ...).
46
+
47
+ ## Capture, void, refund
48
+
49
+ ```python
50
+ from payshen import PayshenError
51
+
52
+ payshen.payments.capture(payment_id, idempotency_key="capture-order-1024") # in full
53
+ payshen.payments.capture(payment_id, amount=3000, idempotency_key="capture-order-1024") # or in part
54
+ payshen.payments.void(payment_id, idempotency_key="void-order-1024") # before capture
55
+
56
+ try:
57
+ payshen.payments.refund(payment_id, amount=2000, idempotency_key="refund-order-1024-1")
58
+ except PayshenError as e:
59
+ # 409 refund_pending: accepted, not failed. The provider confirms a moment later and
60
+ # payment.refunded or payment.refund_failed follows. Do not send it again under a new key.
61
+ if e.code != "refund_pending":
62
+ raise
63
+ ```
64
+
65
+ A capture or void a provider only accepted answers with the payment `PENDING`; it becomes `CAPTURED` or
66
+ `CANCELLED` when the provider confirms, and the webhook follows.
67
+
68
+ ## Read
69
+
70
+ ```python
71
+ payment = payshen.payments.get(payment_id)
72
+ timeline = payshen.payments.events(payment_id)
73
+
74
+ page = payshen.payments.list(limit=50, status="captured")
75
+ for payment in payshen.payments.all(status="captured"):
76
+ ... # every page, newest first
77
+
78
+ for event in payshen.events.all(type="payment.captured"):
79
+ ... # the same events webhooks deliver, for polling
80
+ ```
81
+
82
+ ## Webhooks
83
+
84
+ ```python
85
+ from payshen import SignatureVerificationError, parse_webhook_event
86
+
87
+ # Flask. Django: request.body and request.headers["X-Payshen-Signature"].
88
+ raw = request.get_data() # the RAW body: a body re-encoded from json never verifies
89
+ try:
90
+ event = parse_webhook_event(raw, request.headers.get("X-Payshen-Signature"), os.environ["PAYSHEN_WEBHOOK_SECRET"])
91
+ except SignatureVerificationError:
92
+ return "", 400
93
+
94
+ # Deliveries are retried, so the same event can arrive twice. Record event["id"]
95
+ # and skip one you have already processed.
96
+ if event["type"] == "payment.captured":
97
+ fulfil(event["data"]["reference"])
98
+ return "", 204
99
+ ```
100
+
101
+ The signature proves a delivery came from Payshen and is recent (5 minutes by default); it does not make it new.
102
+ `claim_webhook_event(raw, header, secret, store)` verifies, parses and takes each event once, with a store you back
103
+ with your database:
104
+
105
+ ```python
106
+ class DbEventStore:
107
+ def claim(self, event_id: str) -> bool:
108
+ # PostgreSQL; MySQL: INSERT IGNORE and the affected row count
109
+ cur.execute("INSERT INTO processed_webhook_events (id) VALUES (%s) ON CONFLICT (id) DO NOTHING RETURNING id", (event_id,))
110
+ return cur.fetchone() is not None
111
+ ```
112
+
113
+ During a secret rotation pass both: `parse_webhook_event(raw, header, [new_secret, old_secret])`.
114
+
115
+ ## Errors
116
+
117
+ | Exception | When |
118
+ |---|---|
119
+ | `AuthenticationError` | 401 or 403: a missing, wrong or revoked key, or one not allowed this call |
120
+ | `ValidationError` | 422: refused as sent; `code` names the rule |
121
+ | `RateLimitError` | 429 after the client's retries; `retry_after_seconds` when the API said |
122
+ | `NetworkError` | No answer at all. On a POST, retry with the same idempotency key |
123
+ | `PayshenError` | Everything else, and the parent of all of the above |
124
+
125
+ Every exception has `status`, `code` (`refund_pending`, `not_capturable`, `payment_result_unknown`, ...) and
126
+ `request_id`. Branch on `code`, not on the message.
127
+
128
+ ## Options
129
+
130
+ ```python
131
+ Payshen(key, timeout=20.0, max_retries=3, base_url="https://www.payshen.com")
132
+ ```
133
+
134
+ `payshen.request("GET", "/api/v1/...")` calls any endpoint and returns the whole answer.
135
+
136
+ ## Documentation
137
+
138
+ - API reference and quickstart: <https://www.payshen.com/developers>
139
+ - OpenAPI 3.1: <https://www.payshen.com/api/v1/openapi.json>
140
+
141
+ ## License
142
+
143
+ MIT
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.24"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "payshen"
7
+ version = "0.1.0"
8
+ description = "Python client for the Payshen payment orchestration API: payments, captures, voids, refunds, events and webhook verification."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.9"
13
+ authors = [{ name = "Sodasoft LLC" }]
14
+ keywords = ["payshen", "payments", "payment orchestration", "psp", "webhooks"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Topic :: Office/Business :: Financial",
21
+ "Typing :: Typed",
22
+ ]
23
+ dependencies = []
24
+
25
+ [project.urls]
26
+ Homepage = "https://www.payshen.com/developers"
27
+ Documentation = "https://www.payshen.com/developers"
28
+ "API reference" = "https://www.payshen.com/api/v1/openapi.json"
29
+
30
+ [project.optional-dependencies]
31
+ dev = ["pytest>=8"]
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/payshen"]
35
+
36
+ [tool.pytest.ini_options]
37
+ testpaths = ["tests"]
38
+ filterwarnings = ["error"]
@@ -0,0 +1,45 @@
1
+ """Payshen: the Python client for the Payshen payment orchestration API."""
2
+
3
+ from ._client import Page, Payshen
4
+ from ._http import Response, Transport, TransportError, UrllibTransport
5
+ from ._version import VERSION
6
+ from .errors import (
7
+ AuthenticationError,
8
+ NetworkError,
9
+ PayshenError,
10
+ RateLimitError,
11
+ SignatureVerificationError,
12
+ ValidationError,
13
+ )
14
+ from .webhooks import (
15
+ DEFAULT_TOLERANCE_SECONDS,
16
+ MemoryWebhookEventStore,
17
+ WebhookEventStore,
18
+ claim_webhook_event,
19
+ parse_webhook_event,
20
+ verify_webhook_signature,
21
+ )
22
+
23
+ __version__ = VERSION
24
+
25
+ __all__ = [
26
+ "VERSION",
27
+ "Payshen",
28
+ "Page",
29
+ "PayshenError",
30
+ "AuthenticationError",
31
+ "SignatureVerificationError",
32
+ "ValidationError",
33
+ "RateLimitError",
34
+ "NetworkError",
35
+ "Transport",
36
+ "TransportError",
37
+ "Response",
38
+ "UrllibTransport",
39
+ "DEFAULT_TOLERANCE_SECONDS",
40
+ "verify_webhook_signature",
41
+ "parse_webhook_event",
42
+ "claim_webhook_event",
43
+ "WebhookEventStore",
44
+ "MemoryWebhookEventStore",
45
+ ]