sebpay 0.2.0__py3-none-any.whl

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.
sebpay/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from ._version import __version__
2
+ from .client import SebpayClient
3
+ from .errors import SebpayApiError
4
+ from .webhooks import SIGNATURE_HEADER, verify_signature
5
+
6
+ __all__ = ["SebpayClient", "SebpayApiError", "verify_signature", "SIGNATURE_HEADER"]
sebpay/_version.py ADDED
@@ -0,0 +1,2 @@
1
+ # Sent in the User-Agent header. Keep in sync with pyproject.toml (checked by the tests).
2
+ __version__ = "0.2.0"
sebpay/client.py ADDED
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+
5
+ import requests
6
+
7
+ from ._version import __version__
8
+ from .errors import SebpayApiError
9
+ from .resources.checkout_sessions import CheckoutSessions
10
+
11
+ LIVE_BASE_URL = "https://newapi.sebpay.africa/api/v1"
12
+ SANDBOX_BASE_URL = "https://api.sandbox.sebpay.bj/api/v1"
13
+ DEFAULT_TIMEOUT = 30
14
+
15
+
16
+ class SebpayClient:
17
+ """Entry point for Sebpay hosted checkout. Pass the key pair from your
18
+ dashboard (API développeur): the sandbox and live are separate servers,
19
+ and pk_test_ keys are sent to the sandbox, pk_live_ keys to live.
20
+ `base_url` overrides that choice; `timeout` is in seconds.
21
+ """
22
+
23
+ def __init__(self, public_key: str, secret_key: str, base_url: Optional[str] = None, timeout: float = DEFAULT_TIMEOUT):
24
+ self._public_key = public_key
25
+ self._secret_key = secret_key
26
+ self._timeout = timeout
27
+ self.base_url = (base_url or (SANDBOX_BASE_URL if public_key.startswith("pk_test_") else LIVE_BASE_URL)).rstrip("/")
28
+ self._session = requests.Session()
29
+
30
+ # The transport stays private: the SDK only calls the endpoints it wraps
31
+ self.checkout_sessions = CheckoutSessions(self._request)
32
+
33
+ def _request(self, method: str, path: str, body: Optional[dict] = None) -> Any:
34
+ """Returns the `data` field of a successful response; raises SebpayApiError otherwise."""
35
+ headers = {
36
+ "X-Public-Key": self._public_key,
37
+ "X-Secret-Key": self._secret_key,
38
+ "Accept": "application/json",
39
+ "User-Agent": f"sebpay-python/{__version__}",
40
+ }
41
+
42
+ try:
43
+ response = self._session.request(method, f"{self.base_url}{path}", headers=headers, json=body, timeout=self._timeout)
44
+ except requests.Timeout as e:
45
+ raise SebpayApiError(f"Sebpay request timed out after {self._timeout} s", 0, None, code="TIMEOUT") from e
46
+ except requests.RequestException as e:
47
+ raise SebpayApiError(f"Sebpay request failed: {e}", 0, None, code="NETWORK_ERROR") from e
48
+
49
+ try:
50
+ decoded = response.json()
51
+ except ValueError:
52
+ decoded = None
53
+ error_body = decoded if decoded is not None else response.text
54
+
55
+ if not 200 <= response.status_code < 300 or (isinstance(decoded, dict) and decoded.get("success") is False):
56
+ errors = decoded.get("errors") if isinstance(decoded, dict) else None
57
+ code = errors.get("code") if isinstance(errors, dict) else None
58
+ message = decoded.get("message") if isinstance(decoded, dict) else None
59
+ raise SebpayApiError(
60
+ message if isinstance(message, str) else f"Sebpay API error (HTTP {response.status_code})",
61
+ response.status_code,
62
+ error_body,
63
+ code=code if isinstance(code, str) else None,
64
+ )
65
+
66
+ if not isinstance(decoded, dict) or not isinstance(decoded.get("data"), dict):
67
+ raise SebpayApiError("Sebpay API returned an unexpected response", response.status_code, error_body, code="INVALID_RESPONSE")
68
+
69
+ return decoded["data"]
sebpay/errors.py ADDED
@@ -0,0 +1,17 @@
1
+ from typing import Any, Optional
2
+
3
+
4
+ class SebpayApiError(Exception):
5
+ """Raised for every failed call.
6
+
7
+ status: the HTTP status, or 0 when no response came back (code "NETWORK_ERROR" or "TIMEOUT").
8
+ code: the API's errors.code when it sends one ("EXTERNAL_REFERENCE_CONFLICT", "RATE_LIMITED",
9
+ "WRONG_ENVIRONMENT"…), else None.
10
+ body: the decoded response (its text when it was not JSON).
11
+ """
12
+
13
+ def __init__(self, message: str, status: int, body: Any = None, code: Optional[str] = None):
14
+ super().__init__(message)
15
+ self.status = status
16
+ self.code = code
17
+ self.body = body
sebpay/py.typed ADDED
File without changes
File without changes
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Callable
4
+ from urllib.parse import quote
5
+
6
+ if TYPE_CHECKING:
7
+ from ..types import CheckoutSession, CheckoutSessionDetails
8
+
9
+
10
+ class CheckoutSessions:
11
+ def __init__(self, request: Callable[..., Any]):
12
+ self._request = request
13
+
14
+ def create(self, **params: Any) -> CheckoutSession:
15
+ """Create a hosted checkout session. Returns `checkout_url` — redirect
16
+ your customer's browser there.
17
+
18
+ Params: amount, currency, redirect_url (required), external_reference,
19
+ callback_url, customer_name, customer_email, customer_phone, metadata (optional).
20
+ Sending an external_reference again with the same parameters returns the
21
+ same session with `idempotent_replay` = True.
22
+ """
23
+ return self._request("POST", "/checkout/sessions", params)
24
+
25
+ def retrieve(self, id_or_reference: str) -> CheckoutSessionDetails:
26
+ """Retrieve a session by its id (the `checkout_session_id` of your
27
+ redirect_url), its token or your external_reference. This is the
28
+ server-to-server confirmation: when your customer comes back, deliver
29
+ only if `transaction_status` is "approved" — the redirect alone proves nothing."""
30
+ return self._request("GET", f"/checkout/sessions/{quote(id_or_reference, safe='')}")
sebpay/types.py ADDED
@@ -0,0 +1,46 @@
1
+ """Shapes of what the API returns, for type checkers and editors — plain dicts at runtime."""
2
+
3
+ from typing import Any, Dict, Literal, Optional, TypedDict
4
+
5
+ CheckoutSessionStatus = Literal["open", "completed", "expired", "canceled"]
6
+
7
+
8
+ class _CheckoutSessionFields(TypedDict):
9
+ id: str
10
+ status: CheckoutSessionStatus
11
+ checkout_url: str
12
+ amount: float
13
+ currency: str
14
+ external_reference: Optional[str]
15
+ metadata: Optional[Dict[str, Any]]
16
+ expires_at: str
17
+
18
+
19
+ class CheckoutSession(_CheckoutSessionFields, total=False):
20
+ # present (True) when this external_reference had already created the session
21
+ idempotent_replay: bool
22
+
23
+
24
+ class CheckoutSessionDetails(_CheckoutSessionFields):
25
+ transaction_id: Optional[str]
26
+ # "approved" is the only status that means paid
27
+ transaction_status: Optional[Literal["pending", "approved", "rejected"]]
28
+ created_at: str
29
+
30
+
31
+ class _WebhookPayloadFields(TypedDict):
32
+ transaction_id: str
33
+ external_reference: Optional[str]
34
+ # only final statuses are sent
35
+ status: Literal["approved", "rejected"]
36
+ amount: float
37
+ currency: str
38
+ customer_phone: Optional[str]
39
+ created_at: str
40
+ updated_at: str
41
+
42
+
43
+ class WebhookPayload(_WebhookPayloadFields, total=False):
44
+ # a hosted checkout's webhook names its session and echoes its metadata
45
+ checkout_session_id: str
46
+ metadata: Optional[Dict[str, Any]]
sebpay/webhooks.py ADDED
@@ -0,0 +1,22 @@
1
+ import hashlib
2
+ import hmac
3
+ from typing import Optional, Union
4
+
5
+ SIGNATURE_HEADER = "X-SebPay-Signature"
6
+
7
+
8
+ def verify_signature(raw_body: Union[bytes, str], signature: Optional[str], secret_key: str) -> bool:
9
+ """Verify the `X-SebPay-Signature` header Sebpay sends with every webhook
10
+ callback. Always pass the raw request body — re-serializing a parsed
11
+ payload (e.g. via json.dumps after parsing) can reorder keys and break
12
+ the signature. A missing signature returns False.
13
+ """
14
+ if not isinstance(signature, str) or not signature:
15
+ return False
16
+ if isinstance(raw_body, str):
17
+ raw_body = raw_body.encode("utf-8")
18
+ if not isinstance(raw_body, (bytes, bytearray)):
19
+ return False
20
+
21
+ expected = hmac.new(secret_key.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
22
+ return hmac.compare_digest(expected.encode("ascii"), signature.encode("utf-8"))
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: sebpay
3
+ Version: 0.2.0
4
+ Summary: Official Python SDK for Sebpay hosted checkout — create a checkout session, redirect your customer, confirm the payment and verify webhooks.
5
+ Author: Sebpay
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://sebpay.africa
8
+ Project-URL: Repository, https://github.com/mjims/sebpay-sdk-py
9
+ Project-URL: Issues, https://github.com/mjims/sebpay-sdk-py/issues
10
+ Keywords: sebpay,payment,payments,checkout,mobile-money,africa,webhook,sdk
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: requests>=2.28
27
+ Dynamic: license-file
28
+
29
+ # sebpay-python
30
+
31
+ Official Python SDK for [Sebpay](https://sebpay.africa) **hosted checkout**. It does four things:
32
+ - create a checkout session;
33
+ - redirect your customer to the Sebpay payment page;
34
+ - confirm the payment when they come back;
35
+ - verify Sebpay's webhooks.
36
+
37
+ Its only dependency is `requests`. Direct collections, payouts and reference data are not part of the SDK. They remain available over the plain HTTP API.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install sebpay
43
+ ```
44
+
45
+ Requires Python 3.9 or later. The package ships its type hints (`py.typed`).
46
+
47
+ ## Usage
48
+
49
+ ### 1. Create a session and redirect your customer
50
+
51
+ ```python
52
+ import os
53
+ from sebpay import SebpayClient
54
+
55
+ # Keys from your dashboard (API développeur): pk_test_ keys go to the sandbox server, pk_live_ keys to live.
56
+ sebpay = SebpayClient(os.environ["SEBPAY_PUBLIC_KEY"], os.environ["SEBPAY_SECRET_KEY"])
57
+
58
+ session = sebpay.checkout_sessions.create(
59
+ amount=5000,
60
+ currency="XOF",
61
+ redirect_url="https://yoursite.com/orders/return",
62
+ external_reference=order.id, # your order id: sending it again returns the same session
63
+ callback_url="https://yoursite.com/webhook/sebpay",
64
+ metadata={"order_id": order.id},
65
+ )
66
+
67
+ return redirect(session["checkout_url"])
68
+ ```
69
+
70
+ ### 2. Confirm the payment when your customer comes back
71
+
72
+ Your `redirect_url` receives `?checkout_session_id=<id>`. Coming back proves nothing, so ask Sebpay before you deliver:
73
+
74
+ ```python
75
+ session = sebpay.checkout_sessions.retrieve(request.args["checkout_session_id"])
76
+
77
+ if session["transaction_status"] == "approved":
78
+ ... # Paid: deliver the order (session["external_reference"], session["metadata"]).
79
+ ```
80
+
81
+ `retrieve()` also accepts your `external_reference`.
82
+
83
+ ### 3. Verify the webhook
84
+
85
+ Sebpay POSTs the final status (`approved` or `rejected`) to your `callback_url`. For a checkout, the request also carries `checkout_session_id` and your `metadata`.
86
+
87
+ ```python
88
+ from sebpay import SIGNATURE_HEADER, verify_signature
89
+
90
+ # With Flask:
91
+ if not verify_signature(request.get_data(), request.headers.get(SIGNATURE_HEADER), os.environ["SEBPAY_SECRET_KEY"]):
92
+ return {"error": "Invalid signature"}, 401
93
+
94
+ payload = request.get_json() # payload["status"]: "approved" | "rejected"
95
+ ```
96
+
97
+ - Always pass the **raw** request body. Re-serializing a parsed payload can reorder keys and break the signature.
98
+ - A missing header returns `False`.
99
+
100
+ The shapes of the session and of the webhook payload are described as `TypedDict`s in `sebpay.types`, for editors and type checkers.
101
+
102
+ ## Options
103
+
104
+ ```python
105
+ SebpayClient(public_key, secret_key,
106
+ base_url="http://localhost:8000/api/v1", # only to test against a local API; otherwise chosen from the keys
107
+ timeout=10) # seconds, default 30
108
+ ```
109
+
110
+ `sebpay.base_url` tells which server the client talks to.
111
+
112
+ ## Errors
113
+
114
+ Every failed call raises a `SebpayApiError`, with three attributes:
115
+ - `status`: the HTTP status, or 0 when no response came back;
116
+ - `code`: the API's `errors.code`, or one of the SDK's own codes. Examples: `EXTERNAL_REFERENCE_CONFLICT`, `RATE_LIMITED`, `WRONG_ENVIRONMENT`, `NETWORK_ERROR`, `TIMEOUT`, `INVALID_RESPONSE`. It is `None` otherwise;
117
+ - `body`: the decoded response.
118
+
119
+ ```python
120
+ from sebpay import SebpayApiError
121
+
122
+ try:
123
+ sebpay.checkout_sessions.create(...)
124
+ except SebpayApiError as e:
125
+ if e.code == "EXTERNAL_REFERENCE_CONFLICT":
126
+ ... # This order id was already used with other parameters.
127
+ ```
128
+
129
+ ## Examples
130
+
131
+ See [`examples/`](https://github.com/mjims/sebpay-sdk-py/tree/main/examples). Set `SEBPAY_PUBLIC_KEY` and `SEBPAY_SECRET_KEY`, then:
132
+ - `python examples/create_checkout.py` creates a session and prints its `checkout_url`;
133
+ - `python examples/webhook_server.py` starts a Flask server (`pip install flask`) that handles the webhook and the return page, on `PORT` (default 3002).
134
+
135
+ For local testing, also set `SEBPAY_BASE_URL`.
136
+
137
+ ## Tests
138
+
139
+ `python -m unittest discover -s tests` runs the SDK's offline tests. They cover the transport, against a local HTTP server, and the shared signature vectors.
140
+
141
+ ## Requirements
142
+
143
+ Python 3.9+ and `requests`.
@@ -0,0 +1,14 @@
1
+ sebpay/__init__.py,sha256=UuigNpLJwqXRMhdBn0RrNi-EOvntvdIz-uqsvroIMwI,245
2
+ sebpay/_version.py,sha256=5ZigdfGhEeFjFeCFr4RORO5cwspqJiL1TNKf_jUYSmI,112
3
+ sebpay/client.py,sha256=_0AAJB75Q-Gk74ZKh32jxMPxAx8ht1Ma4iqFQFLXoVY,3144
4
+ sebpay/errors.py,sha256=Im5_LSIqXLoe81Sxli4FKV5E8oyESxIP_JyHSGPI9ZA,631
5
+ sebpay/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ sebpay/types.py,sha256=SlDrabcg7LBHJfxn7J_sveyMl-UatebY_UWX3NTiAhY,1385
7
+ sebpay/webhooks.py,sha256=0--Jxl6SO-S60dwFx3Jf8cygbvbqN-iZq5VP65GO4kY,901
8
+ sebpay/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ sebpay/resources/checkout_sessions.py,sha256=r27Tk9dpbyK3v_XtTsHNKyUZy-TFY79fXiVq7s2Haw4,1355
10
+ sebpay-0.2.0.dist-info/licenses/LICENSE,sha256=G0ZKDwtA6iyZ4FSBTtv7aTtF5_gqfi0pNl2iMRufKTg,1063
11
+ sebpay-0.2.0.dist-info/METADATA,sha256=DrnyCzeuAHngOIZbXvPj98FtMoq3JUbDoCrENh4J4FY,5303
12
+ sebpay-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
13
+ sebpay-0.2.0.dist-info/top_level.txt,sha256=XP-Kn6okZAJvxZZYrd_TWeq8CCzelx3_8C_covvf4Zk,7
14
+ sebpay-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sebpay
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 @@
1
+ sebpay