thunder-solution-sdk 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.
- thunder_solution_sdk-1.0.0/LICENSE +21 -0
- thunder_solution_sdk-1.0.0/PKG-INFO +452 -0
- thunder_solution_sdk-1.0.0/README.md +437 -0
- thunder_solution_sdk-1.0.0/pyproject.toml +44 -0
- thunder_solution_sdk-1.0.0/setup.cfg +4 -0
- thunder_solution_sdk-1.0.0/src/thunder_slip/__init__.py +12 -0
- thunder_solution_sdk-1.0.0/src/thunder_slip/_base_error.py +8 -0
- thunder_solution_sdk-1.0.0/src/thunder_slip/_codec.py +102 -0
- thunder_solution_sdk-1.0.0/src/thunder_slip/_transport.py +282 -0
- thunder_solution_sdk-1.0.0/src/thunder_slip/client.py +860 -0
- thunder_solution_sdk-1.0.0/src/thunder_slip/errors.py +367 -0
- thunder_solution_sdk-1.0.0/src/thunder_slip/files.py +59 -0
- thunder_solution_sdk-1.0.0/src/thunder_slip/models.py +600 -0
- thunder_solution_sdk-1.0.0/src/thunder_slip/py.typed +0 -0
- thunder_solution_sdk-1.0.0/src/thunder_slip/webhook.py +26 -0
- thunder_solution_sdk-1.0.0/src/thunder_slip/webhooks.py +25 -0
- thunder_solution_sdk-1.0.0/src/thunder_solution_sdk.egg-info/PKG-INFO +452 -0
- thunder_solution_sdk-1.0.0/src/thunder_solution_sdk.egg-info/SOURCES.txt +36 -0
- thunder_solution_sdk-1.0.0/src/thunder_solution_sdk.egg-info/dependency_links.txt +1 -0
- thunder_solution_sdk-1.0.0/src/thunder_solution_sdk.egg-info/requires.txt +1 -0
- thunder_solution_sdk-1.0.0/src/thunder_solution_sdk.egg-info/top_level.txt +1 -0
- thunder_solution_sdk-1.0.0/tests/test_billing_matrix.py +58 -0
- thunder_solution_sdk-1.0.0/tests/test_codec.py +42 -0
- thunder_solution_sdk-1.0.0/tests/test_conformance.py +217 -0
- thunder_solution_sdk-1.0.0/tests/test_credential_redaction.py +150 -0
- thunder_solution_sdk-1.0.0/tests/test_deadlines_and_error_privacy.py +223 -0
- thunder_solution_sdk-1.0.0/tests/test_file_contract.py +24 -0
- thunder_solution_sdk-1.0.0/tests/test_helpers.py +176 -0
- thunder_solution_sdk-1.0.0/tests/test_integration_info.py +19 -0
- thunder_solution_sdk-1.0.0/tests/test_integration_job_errors.py +32 -0
- thunder_solution_sdk-1.0.0/tests/test_live_model_edges.py +10 -0
- thunder_solution_sdk-1.0.0/tests/test_package_contract.py +44 -0
- thunder_solution_sdk-1.0.0/tests/test_qr_request.py +60 -0
- thunder_solution_sdk-1.0.0/tests/test_readme.py +33 -0
- thunder_solution_sdk-1.0.0/tests/test_readme_programs.py +111 -0
- thunder_solution_sdk-1.0.0/tests/test_request_types_webhooks.py +49 -0
- thunder_solution_sdk-1.0.0/tests/test_signature_boundaries.py +69 -0
- thunder_solution_sdk-1.0.0/tests/test_transport.py +247 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thunder Solution Co., Ltd.
|
|
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 copies
|
|
9
|
+
of the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
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,452 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: thunder-solution-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Verify Thai bank and TrueMoney slips; check the receiver, amount and duplicates before accepting payment.
|
|
5
|
+
Author: Thunder Solution Co., Ltd.
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/THUNDER-SOLUTION-CO-LTD/sdk-python
|
|
8
|
+
Project-URL: Homepage, https://github.com/THUNDER-SOLUTION-CO-LTD/sdk-python
|
|
9
|
+
Project-URL: Issues, https://github.com/THUNDER-SOLUTION-CO-LTD/sdk-python/issues
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: httpx<1,>=0.28.1
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# Thunder Python SDK
|
|
17
|
+
|
|
18
|
+
[](https://github.com/THUNDER-SOLUTION-CO-LTD/sdk-python/actions/workflows/ci.yml) [](LICENSE)
|
|
19
|
+
|
|
20
|
+
Verify Thai bank transfer slips and TrueMoney Wallet receipts from Python. Check the receiving account, amount and duplicate status before crediting an order.
|
|
21
|
+
|
|
22
|
+
[ภาษาไทย](README.th.md)
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- Verify a bank slip from an image, QR payload, URL or base64.
|
|
27
|
+
- Check whether a transfer pays a linked receiving account for the expected amount.
|
|
28
|
+
- Detect previously used slips before accepting payment.
|
|
29
|
+
- Verify TrueMoney Wallet receipts.
|
|
30
|
+
- Read remaining quota and manage receiving accounts.
|
|
31
|
+
- Submit batches, track background jobs and authenticate webhooks.
|
|
32
|
+
- Generate PromptPay and bill payment QR codes.
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
Python 3.11 through 3.15. Create a service in [the Thunder developer portal](https://developer.thunder.in.th), then copy its API key into your server environment as `THUNDER_API_KEY`. You can instead set `THUNDER_API_KEY_FILE` to a private file containing the key.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
python -m pip install thunder-solution-sdk
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
Save the program below as `verify_payment.py`. Put a transfer slip at `slip.png` and set the API key. In the portal, add your receiving account and link it to the service/branch associated with the key. Change `150.0` to the order amount from your own database, not a value sent by the customer. Each verification can consume quota.
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from pathlib import Path
|
|
50
|
+
from thunder_slip import Client
|
|
51
|
+
from thunder_slip.errors import SlipNotFoundError, SlipPendingError
|
|
52
|
+
from thunder_slip.models import MatchedAccount, VerifyOptions
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main() -> None:
|
|
56
|
+
with Client() as client: # Reads THUNDER_API_KEY from the environment.
|
|
57
|
+
try:
|
|
58
|
+
result = client.verify.bank_by_image(
|
|
59
|
+
Path("slip.png"),
|
|
60
|
+
VerifyOptions(match_account=True, match_amount=150.0, check_duplicate=True),
|
|
61
|
+
)
|
|
62
|
+
except SlipPendingError:
|
|
63
|
+
print("The bank has not made this slip available yet. Check again later.")
|
|
64
|
+
return
|
|
65
|
+
except SlipNotFoundError:
|
|
66
|
+
print("No transfer was found. Ask the customer to check the slip.")
|
|
67
|
+
return
|
|
68
|
+
if result.is_duplicate is not False:
|
|
69
|
+
print("This slip was used before, or duplicate checking was unavailable.")
|
|
70
|
+
return
|
|
71
|
+
if result.is_amount_matched is not True:
|
|
72
|
+
print("The transfer does not match the order amount.")
|
|
73
|
+
return
|
|
74
|
+
if not isinstance(result.matched_account, MatchedAccount):
|
|
75
|
+
print("The transfer did not match a linked receiving account.")
|
|
76
|
+
return
|
|
77
|
+
slip = result.raw_slip
|
|
78
|
+
print("Amount:", result.amount_in_slip)
|
|
79
|
+
print("Date:", slip.date)
|
|
80
|
+
print("Sender:", slip.sender.account.name.th)
|
|
81
|
+
print("Receiver:", slip.receiver.account.name.th)
|
|
82
|
+
print("Transaction:", slip.trans_ref)
|
|
83
|
+
print("Payment checks passed. Record this transaction once for the order.")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
main()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Run `python verify_payment.py`. A successful check prints output like this (illustrative values):
|
|
91
|
+
|
|
92
|
+
Amount: 150.0
|
|
93
|
+
Date: 2026-10-29T14:32:00+07:00
|
|
94
|
+
Sender: ผู้โอนตัวอย่าง
|
|
95
|
+
Receiver: ร้านตัวอย่าง
|
|
96
|
+
Transaction: EXAMPLE-TRANSFER-001
|
|
97
|
+
Payment checks passed. Record this transaction once for the order.
|
|
98
|
+
|
|
99
|
+
Credit the order only after all checks pass. Store the transaction reference with a database uniqueness constraint and update the order in the same transaction, so concurrent requests cannot credit it twice. The SDK does not update your orders.
|
|
100
|
+
|
|
101
|
+
Next steps: [payment checks](#payment-checks), [receiving accounts](#receiving-accounts), [background jobs](#background-jobs), [error handling](#error-handling).
|
|
102
|
+
|
|
103
|
+
## Usage
|
|
104
|
+
|
|
105
|
+
### Verify a bank slip
|
|
106
|
+
|
|
107
|
+
| What you have | Method |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| Image file or uploaded bytes | `client.verify.bank_by_image` |
|
|
110
|
+
| QR payload text | `client.verify.bank_by_payload` |
|
|
111
|
+
| Public image URL | `client.verify.bank_by_url` |
|
|
112
|
+
| Base64 image | `client.verify.bank_by_base64` |
|
|
113
|
+
|
|
114
|
+
Payload means the text decoded from the slip QR, not the transaction reference. Image input accepts a path, `bytes` or a binary file object. A string is a local file path: never pass an untrusted user string, because the SDK will read that file on your server.
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from pathlib import Path
|
|
118
|
+
from thunder_slip import Client
|
|
119
|
+
|
|
120
|
+
with Client() as client:
|
|
121
|
+
result = client.verify.bank_by_payload(Path("payload.txt").read_text().strip())
|
|
122
|
+
print(result.amount_in_slip, result.raw_slip.trans_ref)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Payment checks
|
|
126
|
+
|
|
127
|
+
API v2 returns check results in a successful response: `is_duplicate=True`, `is_amount_matched=False`, or `matched_account=None` means you must reject the payment. A mismatch does not raise an API error. The quick start also rejects missing check results. `matchAccount` checks accounts linked to this service/branch; `matchAmount` compares the transfer against your order total; `checkDuplicate` asks whether this slip was used before.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from thunder_slip import Client
|
|
131
|
+
from thunder_slip.models import VerifyOptions
|
|
132
|
+
|
|
133
|
+
with Client() as client:
|
|
134
|
+
result = client.verify.bank_by_image(
|
|
135
|
+
"slip.png", VerifyOptions(match_account=True, match_amount=150.0, check_duplicate=True)
|
|
136
|
+
)
|
|
137
|
+
print(result.is_duplicate, result.is_amount_matched, result.matched_account)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### TrueMoney Wallet
|
|
141
|
+
|
|
142
|
+
Use a Wallet receipt image. Check the same payment flags before crediting the order; the receipt reference is `raw_slip.transaction_id`.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from thunder_slip import Client
|
|
146
|
+
from thunder_slip.models import VerifyOptions
|
|
147
|
+
|
|
148
|
+
with Client() as client:
|
|
149
|
+
result = client.verify.truewallet_by_image(
|
|
150
|
+
"wallet-slip.png",
|
|
151
|
+
VerifyOptions(match_account=True, match_amount=150.0, check_duplicate=True),
|
|
152
|
+
)
|
|
153
|
+
print(result.amount_in_slip, result.raw_slip.transaction_id)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Account info and quota
|
|
157
|
+
|
|
158
|
+
Read quota before planning a batch. A null remaining quota means the API did not return a numeric limit; it is not zero.
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from thunder_slip import Client
|
|
162
|
+
|
|
163
|
+
with Client() as client:
|
|
164
|
+
info = client.get_info()
|
|
165
|
+
print("Remaining quota:", info.application.quota.remaining)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Receiving accounts
|
|
169
|
+
|
|
170
|
+
This example lists linked accounts with automatic pagination, creates a receiving account, updates its name and links it to the key's branch. Set `RECEIVING_ACCOUNT_NUMBER` to your own account; run the create step once, then reuse the returned id. `list()` and `list_all()` return one page; `iterate_all()` walks all service accounts.
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
import os
|
|
174
|
+
from thunder_slip import Client
|
|
175
|
+
from thunder_slip.models import (
|
|
176
|
+
CreateBankAccountParams,
|
|
177
|
+
ListBankAccountsParams,
|
|
178
|
+
UpdateBankAccountParams,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
with Client() as client:
|
|
182
|
+
for account in client.bank_accounts.iterate(ListBankAccountsParams(limit=50)):
|
|
183
|
+
print(account.id, account.name_th)
|
|
184
|
+
account = client.bank_accounts.create(
|
|
185
|
+
CreateBankAccountParams(
|
|
186
|
+
bank_code="004",
|
|
187
|
+
bank_number=os.environ["RECEIVING_ACCOUNT_NUMBER"],
|
|
188
|
+
name_th="ร้านตัวอย่าง",
|
|
189
|
+
name_en="Example Shop",
|
|
190
|
+
type="JURISTIC",
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
updated = client.bank_accounts.update(
|
|
194
|
+
account.id, UpdateBankAccountParams(name_en="Example Shop")
|
|
195
|
+
)
|
|
196
|
+
print("Updated:", updated.id)
|
|
197
|
+
print("Linked:", client.bank_accounts.link(account.id).linked)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Background jobs
|
|
201
|
+
|
|
202
|
+
Submit QR payloads as a batch and keep each job id. Polling the same job does not submit another verification. `wait_for_job` waits up to ten minutes by default, including HTTP requests and retry delays; inspect the terminal status and payment checks before accepting a result.
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from pathlib import Path
|
|
206
|
+
from thunder_slip import Client
|
|
207
|
+
from thunder_slip.models import BatchSlip
|
|
208
|
+
|
|
209
|
+
with Client() as client:
|
|
210
|
+
payload = Path("payload.txt").read_text().strip()
|
|
211
|
+
batch = client.jobs.submit_bank_batch([BatchSlip(payload=payload, check_duplicate=True)])
|
|
212
|
+
for submitted in batch.jobs:
|
|
213
|
+
job = client.jobs.wait_for_job(submitted.job_id, timeout=600)
|
|
214
|
+
print(job.job_id, job.status, job.result)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Generate a payment QR
|
|
218
|
+
|
|
219
|
+
Set `PROMPTPAY_PHONE` to your PromptPay phone number. For bill payment, use `type="PROMPTPAY"` with `biller_id` and `ref1`; `ref2` is optional. The older `BILL_PAYMENT` type still works for existing integrations. Amounts must be from 0.01 to 9,999,999,999.99 with at most two decimal places; omit the amount for an open-amount QR. Save the image returned by the API:
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
import os
|
|
223
|
+
from pathlib import Path
|
|
224
|
+
from thunder_slip import Client
|
|
225
|
+
from thunder_slip.models import QrRequest
|
|
226
|
+
|
|
227
|
+
with Client() as client:
|
|
228
|
+
qr = client.v1.generate_qr(
|
|
229
|
+
QrRequest(
|
|
230
|
+
type="PROMPTPAY",
|
|
231
|
+
msisdn=os.environ["PROMPTPAY_PHONE"],
|
|
232
|
+
amount=150.0,
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
Path("payment-qr.png").write_bytes(qr.image_bytes())
|
|
236
|
+
print("Saved payment-qr.png")
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
import os
|
|
241
|
+
from thunder_slip import Client
|
|
242
|
+
from thunder_slip.models import QrRequest
|
|
243
|
+
|
|
244
|
+
with Client() as client:
|
|
245
|
+
qr = client.v1.generate_qr(
|
|
246
|
+
QrRequest(
|
|
247
|
+
type="PROMPTPAY",
|
|
248
|
+
biller_id=os.environ["BILLER_ID"],
|
|
249
|
+
ref1=os.environ["BILL_REFERENCE"],
|
|
250
|
+
amount=150.0,
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
print(qr.image)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Legacy API v1
|
|
257
|
+
|
|
258
|
+
Use v2 for new integrations. Existing v1 code can use `client.v1`; it returns the slip directly, and duplicate detection raises `DuplicateSlipError`.
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
from thunder_slip import Client
|
|
262
|
+
from thunder_slip.models import V1VerifyOptions
|
|
263
|
+
|
|
264
|
+
with Client() as client:
|
|
265
|
+
slip = client.v1.verify_bank_by_image("slip.png", V1VerifyOptions(check_duplicate=True))
|
|
266
|
+
print(slip.amount.amount, slip.trans_ref)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Async requests
|
|
270
|
+
|
|
271
|
+
Use `AsyncClient` in an async application and await each operation. Reuse one client for multiple requests; limit concurrency to fit your quota and server resources.
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
import asyncio
|
|
275
|
+
from thunder_slip import AsyncClient
|
|
276
|
+
from thunder_slip.models import VerifyOptions
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
async def main() -> None:
|
|
280
|
+
async with AsyncClient() as client:
|
|
281
|
+
result = await client.verify.bank_by_image(
|
|
282
|
+
"slip.png", VerifyOptions(match_account=True, match_amount=150.0, check_duplicate=True)
|
|
283
|
+
)
|
|
284
|
+
print(result.amount_in_slip, result.is_duplicate, result.is_amount_matched)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
asyncio.run(main())
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## The result object
|
|
291
|
+
|
|
292
|
+
The bank and Wallet result share amount and payment-check fields. Bank details are under `raw_slip`; Wallet uses a different receipt model. Optional fields appear only when the API supplies them: `UNSET` means absent, while `None` means JSON null.
|
|
293
|
+
|
|
294
|
+
| Field | Type | Meaning | Example |
|
|
295
|
+
| --- | --- | --- | --- |
|
|
296
|
+
| `amount_in_slip` | float | Verified transfer amount in baht | 150.0 |
|
|
297
|
+
| `is_duplicate` | optional bool | Whether the slip was used before | false |
|
|
298
|
+
| `is_amount_matched` | optional bool | Whether the transfer matches the order amount | true |
|
|
299
|
+
| `amount_in_order` | optional float | Order amount sent for comparison | 150.0 |
|
|
300
|
+
| `matched_account` | optional account or null | Linked receiving account that matched | account object |
|
|
301
|
+
| `raw_slip.trans_ref` | string | Bank transaction reference | EXAMPLE-TRANSFER-001 |
|
|
302
|
+
| `raw_slip.date` | string | Transfer date and time, including offset when returned | 2026-10-29T14:32:00+07:00 |
|
|
303
|
+
| `raw_slip.sender.account.name.th` | optional string | Sender name in Thai | ผู้โอนตัวอย่าง |
|
|
304
|
+
| `raw_slip.receiver.account.name.th` | optional string | Receiver name in Thai | ร้านตัวอย่าง |
|
|
305
|
+
| `raw_slip.sender.account.bank.account` | optional string | Sender account number as returned, possibly masked | xxx-x-x1234-x |
|
|
306
|
+
| `raw_slip.receiver.account.bank.account` | optional string | Receiver account number as returned, possibly masked | xxx-x-x5678-x |
|
|
307
|
+
| `raw_slip.transaction_id` | string (Wallet) | Wallet transaction reference | EXAMPLE-WALLET-001 |
|
|
308
|
+
| `raw_slip.sender.name` | string (Wallet) | Wallet sender name | ผู้ส่งตัวอย่าง |
|
|
309
|
+
| `raw_slip.receiver.name` | string (Wallet) | Wallet receiver name | ผู้รับตัวอย่าง |
|
|
310
|
+
| `raw_slip.receiver.phone` | string (Wallet) | Wallet receiver phone as returned | 0xx-xxx-1234 |
|
|
311
|
+
|
|
312
|
+
See the [full Python model reference](https://document.thunder.in.th/en/sdk/python).
|
|
313
|
+
|
|
314
|
+
## Configuration
|
|
315
|
+
|
|
316
|
+
| Option | Environment variable | Default | What it does |
|
|
317
|
+
| --- | --- | --- | --- |
|
|
318
|
+
| `api_key` | THUNDER_API_KEY; THUNDER_API_KEY_FILE | environment/file | Explicit key overrides environment; file is the fallback. |
|
|
319
|
+
| `timeout` | none | 30 seconds | Timeout for each HTTP attempt. |
|
|
320
|
+
| `max_retries` | none | 2 | At most two additional attempts when retry rules allow. |
|
|
321
|
+
| `v2_base_url / v1_base_url` | THUNDER_V2_BASE_URL; THUNDER_V1_BASE_URL | https://api.thunder.in.th/v2; https://api.thunder.in.th/v1 | Override the API endpoints. |
|
|
322
|
+
| `http` | none | new HTTP client | Supply a custom client; close it yourself. |
|
|
323
|
+
|
|
324
|
+
The example closes the custom HTTP client as well as the SDK client. With the default client, `with Client()` closes its connections automatically.
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
import httpx
|
|
328
|
+
from thunder_slip import Client
|
|
329
|
+
|
|
330
|
+
with httpx.Client(trust_env=False, follow_redirects=False) as http:
|
|
331
|
+
with Client(timeout=15, max_retries=1, http=http) as client:
|
|
332
|
+
print(client.get_info().application.name)
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Error handling
|
|
336
|
+
|
|
337
|
+
`ThunderError` is the root exception; `ApiError` covers HTTP failures and `RequestTimeoutError` is a `NetworkError`.
|
|
338
|
+
|
|
339
|
+
A recognized API error code selects the error class first; otherwise the SDK uses the HTTP status. Unmapped 4xx responses use `ApiError`.
|
|
340
|
+
|
|
341
|
+
| Error class | HTTP status or stage | When it happens | What to do |
|
|
342
|
+
| --- | --- | --- | --- |
|
|
343
|
+
| `ThunderError` | varies | Base for all SDK failures. | Catch a specific error first. |
|
|
344
|
+
| `ApiError` | HTTP | Base for rejected API requests. | Use status, code and request id for diagnosis. |
|
|
345
|
+
| `AuthenticationError` | 401 | API key is invalid. | Check the key for this service. |
|
|
346
|
+
| `PermissionDeniedError` | 403 | Service, branch or IP cannot call the API. | Check service access and IP restrictions. |
|
|
347
|
+
| `QuotaExceededError` | 403 | Verification quota is exhausted. | Add quota before accepting more slips. |
|
|
348
|
+
| `InvalidRequestError` | 400 | The API rejects an argument. | Correct the request before sending again. |
|
|
349
|
+
| `InvalidImageError` | 400 | Image is unreadable, oversized or unsupported. | Send a readable supported image. |
|
|
350
|
+
| `InvalidUrlError` | 400 | Image or callback URL is refused. | Use a reachable public HTTPS URL. |
|
|
351
|
+
| `DuplicateSlipError` | 400 (v1) | A previously verified slip is submitted with duplicate checking. | Do not credit the same transfer again; inspect slip. |
|
|
352
|
+
| `NotFoundError` | 404 | Resource is missing or outside this branch. | Check the account or job id and branch. |
|
|
353
|
+
| `SlipNotFoundError` | 404 | No transfer or slip QR was found. | Ask for a readable slip from a completed transfer. |
|
|
354
|
+
| `SlipPendingError` | 404 | A recent slip is not available from the bank yet. | Wait before checking again. |
|
|
355
|
+
| `ConflictError` | 409 | Receiving account already exists. | Reuse it instead of creating another. |
|
|
356
|
+
| `RateLimitError` | 429 | Request rate is too high. | Wait for retry_after when present. |
|
|
357
|
+
| `ServerError` | 500, 502, 504; other 5xx except 503 | The API or bank could not finish the request. | Reconcile billed calls before resending. |
|
|
358
|
+
| `ServiceUnavailableError` | 503 | A dependency is unavailable. | Wait and check service status. |
|
|
359
|
+
| `InputError` | before sending | Local validation rejects input. | Fix the argument identified by code. |
|
|
360
|
+
| `NetworkError` | network | Connection fails. | Check connectivity and reconcile the outcome. |
|
|
361
|
+
| `RequestTimeoutError` | network | HTTP request exceeds its timeout. | Check the payment outcome before resubmitting. |
|
|
362
|
+
| `JobWaitTimeoutError` | polling deadline | Job polling exceeds the overall deadline. | Keep last_job and poll the same job later. |
|
|
363
|
+
| `BatchSubmitError` | partial batch | A chunk fails after earlier chunks were accepted. | Resume only the unsent chunks. |
|
|
364
|
+
| `WebhookSignatureError` | before processing | Webhook signature is invalid or missing. | Return 400; do not process the event. |
|
|
365
|
+
|
|
366
|
+
```python
|
|
367
|
+
from thunder_slip import Client
|
|
368
|
+
from thunder_slip.errors import ApiError, NetworkError, QuotaExceededError, SlipNotFoundError
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
with Client() as client:
|
|
372
|
+
result = client.verify.bank_by_image("slip.png")
|
|
373
|
+
print(result.amount_in_slip)
|
|
374
|
+
except SlipNotFoundError:
|
|
375
|
+
print("Ask the customer for a readable slip from a completed transfer.")
|
|
376
|
+
except QuotaExceededError:
|
|
377
|
+
print("Add verification quota before processing more payments.")
|
|
378
|
+
except ApiError as error:
|
|
379
|
+
print("Verification failed:", error.status, error.code, error.request_id)
|
|
380
|
+
except NetworkError:
|
|
381
|
+
print("The outcome is unknown. Reconcile the payment before resubmitting it.")
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
| Situation | Automatic retry (default) |
|
|
385
|
+
| --- | --- |
|
|
386
|
+
| Connection fails before sending | Up to 2 extra attempts, including verification. |
|
|
387
|
+
| HTTP 429 | Up to 2 extra attempts when the delay is at most 60 seconds. |
|
|
388
|
+
| Safe reads and idempotent account linking | Up to 2 extra attempts for transport failures and HTTP 500, 502, 503, 504. |
|
|
389
|
+
| Verification or job submission after sending | Never retry read timeouts or 5xx: another send could consume quota twice. |
|
|
390
|
+
| Other 4xx | No automatic retry; correct the input or permissions. |
|
|
391
|
+
|
|
392
|
+
Retries use exponential backoff with jitter and respect `Retry-After` up to 60 seconds. A failed network call does not prove that a billed operation was never processed.
|
|
393
|
+
|
|
394
|
+
## Webhooks
|
|
395
|
+
|
|
396
|
+
Install Flask with `python -m pip install Flask`, save this as `webhook_app.py`, set `THUNDER_WEBHOOK_SECRET`, then run `python webhook_app.py`. The handler reads the original body and `X-Thunder-Signature`, returns 400 for an invalid signature or malformed event, and acknowledges a verified event with 204. Replace the logging step with durable storage or a queue before using it to confirm payments; process each job id once. Deploy behind HTTPS with a production WSGI server.
|
|
397
|
+
|
|
398
|
+
```python
|
|
399
|
+
import os
|
|
400
|
+
from flask import Flask, Response, request
|
|
401
|
+
from thunder_slip.errors import InputError, WebhookSignatureError
|
|
402
|
+
from thunder_slip.webhooks import construct_event
|
|
403
|
+
|
|
404
|
+
app = Flask(__name__)
|
|
405
|
+
webhook_secret = os.environ["THUNDER_WEBHOOK_SECRET"]
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@app.post("/webhooks/thunder")
|
|
409
|
+
def receive_webhook() -> Response:
|
|
410
|
+
raw_body = request.get_data(cache=False)
|
|
411
|
+
try:
|
|
412
|
+
event = construct_event(
|
|
413
|
+
raw_body, request.headers.get("X-Thunder-Signature"), webhook_secret
|
|
414
|
+
)
|
|
415
|
+
except (WebhookSignatureError, InputError):
|
|
416
|
+
return Response("Invalid webhook", status=400)
|
|
417
|
+
# Save or enqueue the event durably, using job_id to prevent duplicate processing.
|
|
418
|
+
app.logger.info("Verified job %s: %s", event.job_id, event.status)
|
|
419
|
+
return Response(status=204)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
if __name__ == "__main__":
|
|
423
|
+
app.run(port=8000)
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## Security and privacy
|
|
427
|
+
|
|
428
|
+
Keep the API key on your server, never in a browser or mobile application. Public errors and response headers redact the configured key, including percent-encoded and JSON-escaped forms, and always hide authorization headers. Transport causes contain sanitized diagnostics. `sensitive_raw_body()` returns the original error body and can expose secrets; keep it out of logs. Transfer results include personal data such as names and account numbers. Limit access, retention and what you print in application logs.
|
|
429
|
+
|
|
430
|
+
## Versioning and support policy
|
|
431
|
+
|
|
432
|
+
This SDK follows semantic versioning. Breaking public API changes require a major version. The runtime support policy follows versions still supported upstream; changes to the supported range are announced in the [changelog](CHANGELOG.md).
|
|
433
|
+
|
|
434
|
+
## Examples and documentation
|
|
435
|
+
|
|
436
|
+
[Runnable examples](examples/README.md) include one program per operation. Read the [Python SDK guide](https://document.thunder.in.th/en/sdk/python) and [API reference](https://document.thunder.in.th) for all options.
|
|
437
|
+
|
|
438
|
+
## Support
|
|
439
|
+
|
|
440
|
+
For account, quota or billing questions, use the [contact page](https://developer.thunder.in.th/contact) and include the request id if available. Report bugs and suggest features in [issues](https://github.com/THUNDER-SOLUTION-CO-LTD/sdk-python/issues). Never post API keys or real slip images.
|
|
441
|
+
|
|
442
|
+
## Contributing
|
|
443
|
+
|
|
444
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, checks and pull requests. Maintainers follow [RELEASING.md](RELEASING.md).
|
|
445
|
+
|
|
446
|
+
## Security reports
|
|
447
|
+
|
|
448
|
+
Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md), or use the [contact page](https://developer.thunder.in.th/contact). Never report vulnerabilities in public issues.
|
|
449
|
+
|
|
450
|
+
## License
|
|
451
|
+
|
|
452
|
+
[MIT](LICENSE). Copyright (c) 2026 Thunder Solution Co., Ltd.
|