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.
- velirapay-0.1.0/.gitignore +10 -0
- velirapay-0.1.0/CHANGELOG.md +16 -0
- velirapay-0.1.0/LICENSE +21 -0
- velirapay-0.1.0/PKG-INFO +333 -0
- velirapay-0.1.0/README.md +303 -0
- velirapay-0.1.0/pyproject.toml +91 -0
- velirapay-0.1.0/src/velirapay/__init__.py +240 -0
- velirapay-0.1.0/src/velirapay/_client.py +238 -0
- velirapay-0.1.0/src/velirapay/_enums.py +167 -0
- velirapay-0.1.0/src/velirapay/_errors.py +150 -0
- velirapay-0.1.0/src/velirapay/_http.py +193 -0
- velirapay-0.1.0/src/velirapay/_page.py +96 -0
- velirapay-0.1.0/src/velirapay/_params.py +377 -0
- velirapay-0.1.0/src/velirapay/_requester.py +266 -0
- velirapay-0.1.0/src/velirapay/_resources/__init__.py +1 -0
- velirapay-0.1.0/src/velirapay/_resources/_base.py +237 -0
- velirapay-0.1.0/src/velirapay/_resources/account.py +329 -0
- velirapay-0.1.0/src/velirapay/_resources/activity.py +59 -0
- velirapay-0.1.0/src/velirapay/_resources/api_key.py +67 -0
- velirapay-0.1.0/src/velirapay/_resources/charge.py +373 -0
- velirapay-0.1.0/src/velirapay/_resources/coupon.py +87 -0
- velirapay-0.1.0/src/velirapay/_resources/event.py +54 -0
- velirapay-0.1.0/src/velirapay/_resources/invoice.py +169 -0
- velirapay-0.1.0/src/velirapay/_resources/member.py +79 -0
- velirapay-0.1.0/src/velirapay/_resources/overview.py +61 -0
- velirapay-0.1.0/src/velirapay/_resources/payment_link.py +172 -0
- velirapay-0.1.0/src/velirapay/_resources/payout.py +219 -0
- velirapay-0.1.0/src/velirapay/_resources/pro_payment.py +61 -0
- velirapay-0.1.0/src/velirapay/_resources/supported_asset.py +86 -0
- velirapay-0.1.0/src/velirapay/_resources/webhook_endpoint.py +254 -0
- velirapay-0.1.0/src/velirapay/_services/__init__.py +1 -0
- velirapay-0.1.0/src/velirapay/_services/_base.py +117 -0
- velirapay-0.1.0/src/velirapay/_services/account.py +33 -0
- velirapay-0.1.0/src/velirapay/_services/assets.py +18 -0
- velirapay-0.1.0/src/velirapay/_services/charges.py +52 -0
- velirapay-0.1.0/src/velirapay/_services/coupons.py +40 -0
- velirapay-0.1.0/src/velirapay/_services/events.py +28 -0
- velirapay-0.1.0/src/velirapay/_services/invoices.py +44 -0
- velirapay-0.1.0/src/velirapay/_services/management.py +128 -0
- velirapay-0.1.0/src/velirapay/_services/payment_links.py +44 -0
- velirapay-0.1.0/src/velirapay/_services/payouts.py +53 -0
- velirapay-0.1.0/src/velirapay/_services/webhook_endpoints.py +78 -0
- velirapay-0.1.0/src/velirapay/_version.py +3 -0
- velirapay-0.1.0/src/velirapay/_webhooks.py +248 -0
- velirapay-0.1.0/src/velirapay/py.typed +0 -0
- velirapay-0.1.0/tests/__init__.py +0 -0
- velirapay-0.1.0/tests/fixtures/account.json +44 -0
- velirapay-0.1.0/tests/fixtures/charge.json +83 -0
- velirapay-0.1.0/tests/fixtures/coupon.json +19 -0
- velirapay-0.1.0/tests/fixtures/invoice.json +41 -0
- velirapay-0.1.0/tests/fixtures/payment_link.json +36 -0
- velirapay-0.1.0/tests/fixtures/webhook_charge.json +59 -0
- velirapay-0.1.0/tests/fixtures/webhook_invoice.json +52 -0
- velirapay-0.1.0/tests/fixtures/webhook_payment_detected.json +68 -0
- velirapay-0.1.0/tests/fixtures/webhook_payout.json +51 -0
- velirapay-0.1.0/tests/support.py +132 -0
- velirapay-0.1.0/tests/test_account.py +358 -0
- velirapay-0.1.0/tests/test_assets.py +155 -0
- velirapay-0.1.0/tests/test_charges.py +464 -0
- velirapay-0.1.0/tests/test_client.py +237 -0
- velirapay-0.1.0/tests/test_coupons.py +129 -0
- velirapay-0.1.0/tests/test_errors.py +218 -0
- velirapay-0.1.0/tests/test_events.py +56 -0
- velirapay-0.1.0/tests/test_invoices.py +102 -0
- velirapay-0.1.0/tests/test_members.py +224 -0
- velirapay-0.1.0/tests/test_package.py +41 -0
- velirapay-0.1.0/tests/test_pagination.py +119 -0
- velirapay-0.1.0/tests/test_payment_links.py +114 -0
- velirapay-0.1.0/tests/test_payouts.py +117 -0
- velirapay-0.1.0/tests/test_retry.py +217 -0
- velirapay-0.1.0/tests/test_urllib_transport.py +208 -0
- velirapay-0.1.0/tests/test_webhook_endpoints.py +235 -0
- velirapay-0.1.0/tests/test_webhooks.py +285 -0
|
@@ -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.
|
velirapay-0.1.0/LICENSE
ADDED
|
@@ -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.
|
velirapay-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://github.com/VeliraPay/velirapay-python/actions/workflows/tests.yml)
|
|
34
|
+
[](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).
|