velirapay 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.
Files changed (73) hide show
  1. velirapay-0.1.0/.gitignore +10 -0
  2. velirapay-0.1.0/CHANGELOG.md +16 -0
  3. velirapay-0.1.0/LICENSE +21 -0
  4. velirapay-0.1.0/PKG-INFO +333 -0
  5. velirapay-0.1.0/README.md +303 -0
  6. velirapay-0.1.0/pyproject.toml +91 -0
  7. velirapay-0.1.0/src/velirapay/__init__.py +240 -0
  8. velirapay-0.1.0/src/velirapay/_client.py +238 -0
  9. velirapay-0.1.0/src/velirapay/_enums.py +167 -0
  10. velirapay-0.1.0/src/velirapay/_errors.py +150 -0
  11. velirapay-0.1.0/src/velirapay/_http.py +193 -0
  12. velirapay-0.1.0/src/velirapay/_page.py +96 -0
  13. velirapay-0.1.0/src/velirapay/_params.py +377 -0
  14. velirapay-0.1.0/src/velirapay/_requester.py +266 -0
  15. velirapay-0.1.0/src/velirapay/_resources/__init__.py +1 -0
  16. velirapay-0.1.0/src/velirapay/_resources/_base.py +237 -0
  17. velirapay-0.1.0/src/velirapay/_resources/account.py +329 -0
  18. velirapay-0.1.0/src/velirapay/_resources/activity.py +59 -0
  19. velirapay-0.1.0/src/velirapay/_resources/api_key.py +67 -0
  20. velirapay-0.1.0/src/velirapay/_resources/charge.py +373 -0
  21. velirapay-0.1.0/src/velirapay/_resources/coupon.py +87 -0
  22. velirapay-0.1.0/src/velirapay/_resources/event.py +54 -0
  23. velirapay-0.1.0/src/velirapay/_resources/invoice.py +169 -0
  24. velirapay-0.1.0/src/velirapay/_resources/member.py +79 -0
  25. velirapay-0.1.0/src/velirapay/_resources/overview.py +61 -0
  26. velirapay-0.1.0/src/velirapay/_resources/payment_link.py +172 -0
  27. velirapay-0.1.0/src/velirapay/_resources/payout.py +219 -0
  28. velirapay-0.1.0/src/velirapay/_resources/pro_payment.py +61 -0
  29. velirapay-0.1.0/src/velirapay/_resources/supported_asset.py +86 -0
  30. velirapay-0.1.0/src/velirapay/_resources/webhook_endpoint.py +254 -0
  31. velirapay-0.1.0/src/velirapay/_services/__init__.py +1 -0
  32. velirapay-0.1.0/src/velirapay/_services/_base.py +117 -0
  33. velirapay-0.1.0/src/velirapay/_services/account.py +33 -0
  34. velirapay-0.1.0/src/velirapay/_services/assets.py +18 -0
  35. velirapay-0.1.0/src/velirapay/_services/charges.py +52 -0
  36. velirapay-0.1.0/src/velirapay/_services/coupons.py +40 -0
  37. velirapay-0.1.0/src/velirapay/_services/events.py +28 -0
  38. velirapay-0.1.0/src/velirapay/_services/invoices.py +44 -0
  39. velirapay-0.1.0/src/velirapay/_services/management.py +128 -0
  40. velirapay-0.1.0/src/velirapay/_services/payment_links.py +44 -0
  41. velirapay-0.1.0/src/velirapay/_services/payouts.py +53 -0
  42. velirapay-0.1.0/src/velirapay/_services/webhook_endpoints.py +78 -0
  43. velirapay-0.1.0/src/velirapay/_version.py +3 -0
  44. velirapay-0.1.0/src/velirapay/_webhooks.py +248 -0
  45. velirapay-0.1.0/src/velirapay/py.typed +0 -0
  46. velirapay-0.1.0/tests/__init__.py +0 -0
  47. velirapay-0.1.0/tests/fixtures/account.json +44 -0
  48. velirapay-0.1.0/tests/fixtures/charge.json +83 -0
  49. velirapay-0.1.0/tests/fixtures/coupon.json +19 -0
  50. velirapay-0.1.0/tests/fixtures/invoice.json +41 -0
  51. velirapay-0.1.0/tests/fixtures/payment_link.json +36 -0
  52. velirapay-0.1.0/tests/fixtures/webhook_charge.json +59 -0
  53. velirapay-0.1.0/tests/fixtures/webhook_invoice.json +52 -0
  54. velirapay-0.1.0/tests/fixtures/webhook_payment_detected.json +68 -0
  55. velirapay-0.1.0/tests/fixtures/webhook_payout.json +51 -0
  56. velirapay-0.1.0/tests/support.py +132 -0
  57. velirapay-0.1.0/tests/test_account.py +358 -0
  58. velirapay-0.1.0/tests/test_assets.py +155 -0
  59. velirapay-0.1.0/tests/test_charges.py +464 -0
  60. velirapay-0.1.0/tests/test_client.py +237 -0
  61. velirapay-0.1.0/tests/test_coupons.py +129 -0
  62. velirapay-0.1.0/tests/test_errors.py +218 -0
  63. velirapay-0.1.0/tests/test_events.py +56 -0
  64. velirapay-0.1.0/tests/test_invoices.py +102 -0
  65. velirapay-0.1.0/tests/test_members.py +224 -0
  66. velirapay-0.1.0/tests/test_package.py +41 -0
  67. velirapay-0.1.0/tests/test_pagination.py +119 -0
  68. velirapay-0.1.0/tests/test_payment_links.py +114 -0
  69. velirapay-0.1.0/tests/test_payouts.py +117 -0
  70. velirapay-0.1.0/tests/test_retry.py +217 -0
  71. velirapay-0.1.0/tests/test_urllib_transport.py +208 -0
  72. velirapay-0.1.0/tests/test_webhook_endpoints.py +235 -0
  73. velirapay-0.1.0/tests/test_webhooks.py +285 -0
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ /build/
4
+ /dist/
5
+ *.egg-info/
6
+ /.venv/
7
+ /.mypy_cache/
8
+ /.ruff_cache/
9
+ /.idea/
10
+ /.vscode/
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ All notable changes to this library are documented here. It follows [Semantic Versioning](https://semver.org).
4
+
5
+ ## 0.1.0 - 2026-11-12
6
+
7
+ The first version, covering what the PHP library covers as of its 0.6.5:
8
+
9
+ - `VeliraPayClient` with `account`, `assets`, `charges`, `payment_links`, `invoices`, `coupons`, `payouts`, `payout_addresses`, `events`, `webhook_endpoints`, `overview`, `activity`, `api_keys`, `members`, `invitations` and `billing`, each answering with typed objects that keep every attribute the API sent (`get()`, `to_dict()`).
10
+ - Parameters as dicts under the API's own names, typed with a `TypedDict` per endpoint, in which a key set to `None` is sent as `null`. Enum members, a `datetime` with a time zone, a `date` and a `Decimal` are sent the way the API expects.
11
+ - `Page` for lists, with `next_page()` and `auto_paging_iterator()`, and `iterator()` on every paged list.
12
+ - An error per kind of failure, all of them a `VeliraPayError`: `AuthenticationError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `ValidationError`, `RateLimitError`, `ServerError`, `InvalidRequestError`, `UnexpectedResponseError`, `ApiConnectionError`, `SignatureVerificationError` and `InvalidArgumentError`.
13
+ - Retries of network errors, rate limits and outages, with an `Idempotency-Key` on every write so a retried one is carried out once.
14
+ - `Webhook.construct_event()`, which checks the signature, the second one sent for a day after a secret is rotated, and its age.
15
+ - `Asset` also has `BTC_LIGHTNING`, `ETH_BASE`, `ETH_ARBITRUM` and `POL`, which the API defines, charges and coins read the `network_surcharge` the API sends, and charges their `payment_request`.
16
+ - Requests through `urllib` from the standard library, or any transport you give the client, for Python 3.11 or later, without dependencies.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VeliraPay
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,333 @@
1
+ Metadata-Version: 2.5
2
+ Name: velirapay
3
+ Version: 0.1.0
4
+ Summary: The official Python library for VeliraPay crypto payments: charges, payment links, invoices, payouts, events and webhook verification.
5
+ Project-URL: Homepage, https://velirapay.com
6
+ Project-URL: Documentation, https://velirapay.com/docs
7
+ Project-URL: Repository, https://github.com/VeliraPay/velirapay-python
8
+ Project-URL: Issues, https://github.com/VeliraPay/velirapay-python/issues
9
+ Project-URL: Changelog, https://github.com/VeliraPay/velirapay-python/blob/main/CHANGELOG.md
10
+ Author: VeliraPay
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: api,bitcoin,crypto,ethereum,payment-gateway,payments,sdk,solana,velirapay,webhooks
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
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.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Programming Language :: Python :: 3.15
24
+ Classifier: Topic :: Office/Business :: Financial
25
+ Classifier: Topic :: Office/Business :: Financial :: Point-Of-Sale
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.11
29
+ Description-Content-Type: text/markdown
30
+
31
+ # VeliraPay for Python
32
+
33
+ [![Tests](https://github.com/VeliraPay/velirapay-python/actions/workflows/tests.yml/badge.svg)](https://github.com/VeliraPay/velirapay-python/actions/workflows/tests.yml)
34
+ [![License](https://img.shields.io/github/license/VeliraPay/velirapay-python)](https://github.com/VeliraPay/velirapay-python/blob/main/LICENSE)
35
+
36
+ The official Python library for [VeliraPay](https://velirapay.com), the crypto payment processor. It covers the v1 API (the coins on offer, charges, invoices, payment links, coupons, payouts, events, webhook endpoints and your account) and verifies the webhooks VeliraPay sends you. It is fully typed and has no dependencies.
37
+
38
+ - [Requirements](#requirements)
39
+ - [Installation](#installation)
40
+ - [Charging a customer](#charging-a-customer)
41
+ - [Verifying webhooks](#verifying-webhooks)
42
+ - [Which one to use](#which-one-to-use)
43
+ - [What the client covers](#what-the-client-covers)
44
+ - [Lists and pagination](#lists-and-pagination)
45
+ - [Errors](#errors)
46
+ - [Retries and idempotency](#retries-and-idempotency)
47
+ - [Test mode](#test-mode)
48
+ - [Configuration](#configuration)
49
+
50
+ The full API reference is at [velirapay.com/docs](https://velirapay.com/docs).
51
+
52
+ ## Requirements
53
+
54
+ Python 3.11 or later. The library uses the standard library alone.
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ pip install velirapay
60
+ ```
61
+
62
+ ## Charging a customer
63
+
64
+ Create an API key in the dashboard under **Developers → API keys**. Keep it on your server: anyone holding it can create charges and read your payments.
65
+
66
+ To charge a customer, create a charge with what you already know about them, and send them to its checkout page. With Flask:
67
+
68
+ ```python
69
+ import os
70
+
71
+ from flask import Flask, redirect
72
+ from velirapay import VeliraPayClient
73
+
74
+ app = Flask(__name__)
75
+ velirapay = VeliraPayClient(os.environ["VELIRAPAY_API_KEY"])
76
+
77
+
78
+ @app.post("/orders/1042/pay")
79
+ def pay():
80
+ charge = velirapay.charges.create(
81
+ {
82
+ "amount": "49.90",
83
+ "currency": "EUR",
84
+ "asset": "BTC",
85
+ "customer": {"email": "ada@example.com", "name": "Ada Lovelace"},
86
+ "metadata": {"order_id": "1042"},
87
+ "success_url": "https://example.com/orders/1042",
88
+ },
89
+ idempotency_key="order-1042",
90
+ )
91
+
92
+ return redirect(charge.checkout_url, code=303)
93
+ ```
94
+
95
+ They pay there, and VeliraPay tells your server about it with a [webhook](#verifying-webhooks). The exchange rate is locked when the charge is created, and the customer has 30 minutes to pay.
96
+
97
+ Parameters are a dict under the names the API gives them, as the [API reference](https://velirapay.com/docs) lists them. A key you leave out is not sent and a key set to `None` is sent as `null`, which is how a field is cleared; that difference is why they are not keyword arguments, and so is `from`, a Python keyword the activity log filters by. They are typed (`ChargeCreateParams` and so on), so your editor and type checker know every key. Enum members such as `Asset.BTC` can stand in for their values, and a `datetime` with a time zone, a `date` or a `Decimal` is sent the way the API expects.
98
+
99
+ Objects come back with typed attributes: `charge.checkout_url`, `charge.asset_amount`, `charge.customer.ip_address`. Amounts are always strings, such as `"49.90"`, never floats, so no precision is lost; `Decimal(charge.asset_amount)` does arithmetic with them. Times are `datetime` objects in UTC, and a day such as `invoice.due_at` is a `date`. `checkout_url` is typed `str | None` because the charges in webhooks leave it out; a charge the API answers with always has one.
100
+
101
+ A charge from your server comes from your server rather than the customer, so pass what you know about them as `customer` (`email`, `name`, `ip_address`, `user_agent`, `reference`, `phone`, `country` and `metadata`); it helps you spot fraud before you ship anything. `charge.customer` reads it back.
102
+
103
+ ## Verifying webhooks
104
+
105
+ Add an endpoint in the dashboard under **Developers → Webhooks** and copy its signing secret (`whsec_…`). `Webhook.construct_event()` checks a delivery's signature and parses it. Pass it the **raw** body: a body parsed and encoded again no longer matches its signature. With Flask:
106
+
107
+ ```python
108
+ import os
109
+
110
+ from flask import Flask, request
111
+ from velirapay import SignatureVerificationError, Webhook
112
+
113
+ app = Flask(__name__)
114
+
115
+
116
+ @app.post("/webhooks/velirapay")
117
+ def velirapay_webhook():
118
+ try:
119
+ event = Webhook.construct_event(
120
+ request.get_data(),
121
+ request.headers.get(Webhook.SIGNATURE_HEADER),
122
+ os.environ["VELIRAPAY_WEBHOOK_SECRET"],
123
+ )
124
+ except SignatureVerificationError:
125
+ return "", 400
126
+
127
+ # Sent when the endpoint is added or moved: it receives nothing else until it answers.
128
+ if event.is_verification():
129
+ return event.challenge_response(), 200, {"Content-Type": "application/json"}
130
+
131
+ # The same event can arrive more than once: remember this key, and skip the events you have handled.
132
+ key = event.event_id or event.id
133
+
134
+ if event.is_type("charge.paid"):
135
+ ... # Fulfil the order event.charge.metadata["order_id"].
136
+
137
+ return "", 200
138
+ ```
139
+
140
+ With FastAPI, read the body as bytes before anything parses it:
141
+
142
+ ```python
143
+ import os
144
+
145
+ from fastapi import FastAPI, Request, Response
146
+ from velirapay import SignatureVerificationError, Webhook
147
+
148
+ app = FastAPI()
149
+
150
+
151
+ @app.post("/webhooks/velirapay")
152
+ async def velirapay_webhook(request: Request) -> Response:
153
+ try:
154
+ event = Webhook.construct_event(
155
+ await request.body(),
156
+ request.headers.get(Webhook.SIGNATURE_HEADER),
157
+ os.environ["VELIRAPAY_WEBHOOK_SECRET"],
158
+ )
159
+ except SignatureVerificationError:
160
+ return Response(status_code=400)
161
+
162
+ if event.is_verification():
163
+ return Response(event.challenge_response(), media_type="application/json")
164
+
165
+ if event.is_type("charge.paid"):
166
+ ... # Fulfil the order event.charge.metadata["order_id"].
167
+
168
+ return Response(status_code=200)
169
+ ```
170
+
171
+ In Django, the raw body is `request.body` and the header `request.headers.get(Webhook.SIGNATURE_HEADER)`.
172
+
173
+ Some things to know:
174
+
175
+ - **Answer the verification challenge.** When an endpoint is added, or its URL changes, VeliraPay sends it a signed `webhook.verification` delivery and sends it nothing else until it answers with HTTP 200 and the challenge, as `event.challenge_response()` gives it. Endpoint URLs must use `https://`.
176
+ - **A second signature for a day after a rotation.** For a day after you rotate the signing secret, the `X-VeliraPay-Signature` header carries a `v1` made with the old secret too, so a receiver that still has the old one keeps working. `construct_event()` accepts a delivery whichever `v1` matches.
177
+ - **Signatures expire.** A signature more than 5 minutes from the current time is rejected, which stops an intercepted delivery from being replayed. Change the limit with the `tolerance` argument, in seconds; `0` turns the age check off.
178
+ - **Answer quickly.** A delivery counts as delivered when your endpoint answers with a 2xx status within 15 seconds, so queue slow work. Redirects are not followed.
179
+ - **Failed deliveries are retried.** A delivery is tried 14 times over about 3 days.
180
+ - **Skip duplicates by event.** A retried delivery keeps its id (`event.id`), but each endpoint gets its own copy of an event under its own id. `event.event_id` is the same on every copy, so `event.event_id or event.id` is the key to remember.
181
+ - **Fulfil once.** A paid invoice sends both `charge.paid`, for the charge that paid it, and `invoice.paid`. Fulfil on one of them.
182
+ - **Deliveries can arrive out of order.** When the order matters, fetch the current state with `velirapay.charges.retrieve(event.charge.id)`.
183
+
184
+ `charge.payment_detected` arrives as soon as a transfer is seen, before it is confirmed: tell the customer, but do not fulfil. It and `charge.late_payment` carry that transfer as `event.transaction`; `payout.sent` and `payout.confirmed` carry the payout as `event.payout`.
185
+
186
+ To test your handler, sign a payload yourself: `Webhook.signature_header(payload, "whsec_test")` gives the header VeliraPay would send.
187
+
188
+ ## Which one to use
189
+
190
+ | When | Use | |
191
+ | --- | --- | --- |
192
+ | You are asking one customer for one payment | A charge | The default. It has one checkout page for that one payment, with the rate locked for 30 minutes. |
193
+ | The customer should choose the coin, or pays later | An invoice | It needs their name and email, has no timer, can be itemised and reminded, and lets them pick the coin on its page. |
194
+ | Many customers pay through the same public page | A payment link, created once | A product sold again and again, a tip jar, a link on your site. |
195
+
196
+ Do not create a new payment link for each customer or each order.
197
+
198
+ A charge needs its coin up front, because its exchange rate, amount and deposit address depend on it. Ask the customer on your own page, and offer them the coins from `velirapay.assets.list()`, which also gives the lowest price each can be charged at.
199
+
200
+ ## What the client covers
201
+
202
+ | Attribute | Methods |
203
+ | --- | --- |
204
+ | `charges` | `create`, `retrieve`, `list`, `iterator`, `send`, `cancel`, `accept`, `record_refund`, `simulate` |
205
+ | `invoices` | `create`, `retrieve`, `list`, `iterator`, `send`, `void` |
206
+ | `payment_links` | `create`, `retrieve`, `update`, `list`, `iterator`, `archive`, `restore` |
207
+ | `coupons` | `create`, `retrieve`, `update`, `list`, `iterator`, `delete` |
208
+ | `assets` | `list` |
209
+ | `payouts` | `retrieve` (by the charge's id), `list`, `iterator`, `summary` |
210
+ | `payout_addresses` | `list`, `update`, `delete` |
211
+ | `events` | `retrieve`, `list`, `iterator` |
212
+ | `webhook_endpoints` | `create`, `retrieve`, `update`, `delete`, `list`, `iterator`, `verify`, `test`, `rotate_secret`, `deliveries`, `retrieve_delivery`, `retry_delivery` |
213
+ | `account` | `retrieve`, `update`, `update_payment_settings` |
214
+ | `overview` | `retrieve` |
215
+ | `activity` | `list`, `iterator` |
216
+ | `api_keys` | `create`, `update`, `revoke`, `list`, `iterator` |
217
+ | `members` | `update`, `remove`, `list`, `iterator` |
218
+ | `invitations` | `create`, `cancel`, `list`, `iterator` |
219
+ | `billing` | `start_pro_payment` |
220
+
221
+ Each answers with an object such as a `Charge`. `get("field")` reads an attribute this version of the library does not know yet, and `to_dict()` gives the object as the API sent it, such as to store it. Status values are in enums, such as `ChargeStatus.PAID` and `Asset.USDT`, and objects have helpers such as `charge.is_paid()` and `event.is_type(EventType.CHARGE_PAID)`.
222
+
223
+ Most endpoints need a permission, which a key with full access has, except the sensitive ones, such as `webhook_endpoints:write` and `api_keys:write`: only a key made with them ticked has those. A key without the permission an endpoint needs gets a `PermissionDeniedError` naming it.
224
+
225
+ For an endpoint the library has no method for yet, send the request yourself: `velirapay.request("GET", "/v1/charges", {"per_page": 5})` answers with the response, whose `json()` decodes it.
226
+
227
+ ## Lists and pagination
228
+
229
+ `list()` methods answer with one page, newest first (members oldest first). Pass `page` and `per_page` (up to 100, 25 by default):
230
+
231
+ ```python
232
+ page = velirapay.charges.list({"status": "paid", "per_page": 100})
233
+
234
+ for charge in page:
235
+ ...
236
+
237
+ page.total
238
+ page.has_more()
239
+ following = page.next_page() # None on the last page
240
+ ```
241
+
242
+ `iterator()` walks every page, fetching each one as it is reached:
243
+
244
+ ```python
245
+ for charge in velirapay.charges.iterator({"status": "paid"}):
246
+ ...
247
+ ```
248
+
249
+ `assets.list()` answers with a single page holding every coin, and `payout_addresses.list()` and `payouts.summary()` with lists.
250
+
251
+ ## Errors
252
+
253
+ A failed request raises an error carrying the API's message. They are all importable from `velirapay` and are all a `VeliraPayError`:
254
+
255
+ | Error | When |
256
+ | --- | --- |
257
+ | `AuthenticationError` | The API key is missing, wrong or revoked (401). |
258
+ | `PermissionDeniedError` | The API key may not do this (403): a restricted key without the permission the endpoint needs, or a key used from an IP address it does not allow. |
259
+ | `NotFoundError` | The object does not exist, or belongs to the other mode (404). |
260
+ | `ConflictError` | The object cannot make that change (409), such as canceling a charge that is no longer pending, or an idempotency key reused for a different request. |
261
+ | `ValidationError` | The request has invalid fields (422). `errors()` lists them by field, and `first_error("amount")` gives the first message for one. |
262
+ | `RateLimitError` | Too many requests (429). `retry_after()` says how many seconds to wait. |
263
+ | `ServerError` | VeliraPay had a problem (5xx). |
264
+ | `InvalidRequestError` | Any other 4xx, or a redirect, which is not followed. |
265
+ | `ApiConnectionError` | The API could not be reached, or its answer not read, after any retries. |
266
+ | `SignatureVerificationError` | A webhook's signature is missing, does not match, or is too old. |
267
+ | `InvalidArgumentError` | The library was given something it cannot use, such as an API key that does not start with `vp_live_` or `vp_test_`. It is a `ValueError` too. |
268
+
269
+ Every error from an API answer is an `ApiError`, with `status`, `body` (the decoded response), `header(name)` and `retry_after()`.
270
+
271
+ ```python
272
+ from velirapay import ApiError, ValidationError
273
+
274
+ try:
275
+ velirapay.charges.create({"amount": "0", "currency": "EUR", "asset": "BTC"})
276
+ except ValidationError as error:
277
+ error.errors() # {"amount": ["…"]}
278
+ except ApiError as error:
279
+ error.status
280
+ ```
281
+
282
+ ## Retries and idempotency
283
+
284
+ Requests that fail with a network error, a rate limit (429) or an outage (502, 503, 504) are tried again up to twice, waiting a little longer each time, or as long as the API's `Retry-After` header asks. A 500 is retried for reads only, since a write may have been carried out. A `Retry-After` longer than `max_retry_after` (10 seconds unless [configured](#configuration) otherwise) is not waited out: the error is raised straight away, and its `retry_after()` says how long to wait.
285
+
286
+ While retries are on, each write carries an `Idempotency-Key` header, so the API carries out a retried request once. The library makes up a key per call; pass your own, such as an order number, to make a call safe to repeat across processes:
287
+
288
+ ```python
289
+ charge = velirapay.charges.create(params, idempotency_key="order-1042")
290
+ ```
291
+
292
+ For 24 hours, a request repeating a key gets the first successful response, and `velirapay.last_response().was_replayed()` is then true. Reusing a key with a different body raises a `ConflictError`. A key covers one account, one mode, one method and one path, and is at most 255 characters.
293
+
294
+ ## Test mode
295
+
296
+ Keys starting with `vp_test_` work in test mode: charges are paid with test coins on each chain's test network, and nothing touches real funds. Test and live objects are fully separate, and each mode has its own webhooks.
297
+
298
+ ```python
299
+ velirapay.is_test_mode() # read from the key's prefix
300
+
301
+ # Pay a test charge without any coins: it becomes paid, and the webhooks go out as for a real payment.
302
+ velirapay.charges.simulate("k3v9x2m7q8wz")
303
+ velirapay.charges.simulate("k3v9x2m7q8wz", {"amount": "0.001"}) # less than is due, to try an underpayment
304
+ ```
305
+
306
+ ## Configuration
307
+
308
+ ```python
309
+ velirapay = VeliraPayClient(
310
+ os.environ["VELIRAPAY_API_KEY"],
311
+ max_retries=2, # 0 turns retries off
312
+ max_retry_after=10, # the longest Retry-After, in seconds, waited out before trying again
313
+ timeout=30, # seconds an attempt may wait to connect, and each time it waits for more of the answer
314
+ app_info="MyShop/2.1", # added to the User-Agent
315
+ )
316
+ ```
317
+
318
+ Requests go through `urllib` from the standard library, which follows the `HTTPS_PROXY` and `NO_PROXY` environment variables. To trust other certificates, pass `transport=UrllibTransport(ssl_context=...)`. Any callable that takes an `HttpRequest` and answers with an `ApiResponse` can be the `transport`, such as one that sends requests through your own HTTP client or one that answers your tests; it raises an `OSError` when the API cannot be reached, which is retried like any network error.
319
+
320
+ A client can be shared between threads, and `last_response()` gives the response to the most recent request of the thread that asks. The client is synchronous; from async code, run its calls in a thread, such as with `await asyncio.to_thread(velirapay.charges.create, params)`.
321
+
322
+ ## Development
323
+
324
+ ```bash
325
+ python -m pip install -e . --group dev
326
+ python -m unittest # the tests
327
+ mypy # the types, in strict mode
328
+ ruff format --check && ruff check
329
+ ```
330
+
331
+ ## License
332
+
333
+ MIT. See [LICENSE](https://github.com/VeliraPay/velirapay-python/blob/main/LICENSE).