Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Check our main [developer changelog](https://developer.paddle.com/?utm_source=dx
- Non-catalog discounts on Transactions, see [changelog](https://developer.paddle.com/changelog/2025/custom-discounts?utm_source=dx&utm_medium=paddle-python-sdk)
- Support `retained_fee` field on totals objects to show the fees retained by Paddle for the adjustment.
- Added support for new payment methods `blik`, `mb_way`, `pix` and `upi`. See [related changelog](https://developer.paddle.com/changelog/2025/blik-mbway-payment-methods?utm_source=dx&utm_medium=paddle-python-sdk).
- `ApiError` will now have `retry_after` property set for [too_many_requests](https://developer.paddle.com/errors/shared/too_many_requests?utm_source=dx&utm_medium=paddle-python-sdk) errors

## 1.10.0 - 2025-08-15

Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,22 @@ paddle = Client('PADDLE_API_SECRET_KEY')
deleted_product = paddle.products.delete('PRODUCT_ID')
```

### Error Handling

If a request fails, Paddle raises an `ApiError` that contains the same information as [errors returned by the API](https://developer.paddle.com/api-reference/about/errors?utm_source=dx&utm_medium=paddle-python-sdk). You can use the `code` attribute to search an error in [the error reference](https://developer.paddle.com/errors/overview?utm_source=dx&utm_medium=paddle-python-sdk) and to handle the error in your app. Validation errors also return an array of `errors` that tell you which fields failed validation. The `retry_after` property will be set for `too_many_requests` errors.

This example shows how to handle an error with the code `conflict`:

```python
from paddle_billing.Exceptions.ApiError import ApiError

try:
# Call functions from the SDK
except ApiError as error:
# error.error_code will always follow the error code defined in our documentation
if error.error_code == 'conflict':
# Handle Conflict error
```

## Resources

Expand Down
1 change: 1 addition & 0 deletions examples/get_products.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
print(f"detail: {error.detail}")
print(f"field_errors: {error.field_errors}")
print(f"response.status_code: {error.response.status_code}")
print(f"retry_after: {error.retry_after}")
except Exception as error:
log.error(f"We received an error listing products: {error}")

Expand Down
8 changes: 5 additions & 3 deletions paddle_billing/Exceptions/ApiError.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
from paddle_billing.Exceptions.FieldError import FieldError

from requests import HTTPError
from requests import HTTPError, Response


class ApiError(HTTPError):
def __init__(self, response, error_type, error_code, detail, docs_url, *field_errors):
def __init__(self, response: Response, error_type: str, error_code: str, detail: str, docs_url: str, *field_errors):
super().__init__(detail, response=response)
self.error_type = error_type
self.error_code = error_code
self.detail = detail
self.docs_url = docs_url
self.field_errors = field_errors
retry_after = response.headers.get("Retry-After")
self.retry_after = int(retry_after) if retry_after else None

def __repr__(self):
return (
Expand All @@ -19,6 +21,6 @@ def __repr__(self):
)

@classmethod
def from_error_data(cls, response, error):
def from_error_data(cls, response: Response, error):
field_errors = [FieldError(fe["field"], fe["message"]) for fe in error.get("errors", [])]
return cls(response, error["type"], error["code"], error["detail"], error["documentation_url"], *field_errors)
29 changes: 29 additions & 0 deletions tests/Functional/Client/test_Client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ class TestClient:
"expected_reason",
"expected_response_body",
"expected_exception",
"headers",
"expected_retry_after",
],
[
(
Expand All @@ -35,6 +37,8 @@ class TestClient:
"meta": {"request_id": "f00bb3ca-399d-4686-889c-50b028f4c912"},
},
ApiError,
{},
None,
),
(
404,
Expand All @@ -49,6 +53,8 @@ class TestClient:
"meta": {"request_id": "f00bb3ca-399d-4686-889c-50b028f4c912"},
},
ApiError,
{},
None,
),
(
400,
Expand All @@ -63,12 +69,31 @@ class TestClient:
"meta": {"request_id": "f00bb3ca-399d-4686-889c-50b028f4c912"},
},
AddressApiError,
{},
None,
),
(
429,
"Too Many Requests",
{
"error": {
"type": "request_error",
"code": "too_many_requests",
"detail": "IP address exceeded the allowed rate limit. Retry after the number of seconds in the Retry-After header.",
"documentation_url": "https://developer.paddle.com/errors/shared/too_many_requests",
},
"meta": {"request_id": "f00bb3ca-399d-4686-889c-50b028f4c912"},
},
ApiError,
{"Retry-After": "42"},
42,
),
],
ids=[
"Returns bad_request response",
"Returns not_found response",
"Returns address_location_not_allowed response",
"Returns too_many_requests response",
],
)
def test_post_raw_returns_error_response(
Expand All @@ -80,6 +105,8 @@ def test_post_raw_returns_error_response(
expected_reason,
expected_response_body,
expected_exception,
headers,
expected_retry_after,
):
expected_request_url = f"{test_client.base_url}/some/url"
expected_request_body = {"some_property": "some value"}
Expand All @@ -88,6 +115,7 @@ def test_post_raw_returns_error_response(
status_code=expected_response_status,
text=dumps(expected_response_body),
reason=expected_reason,
headers=headers,
)

with raises(expected_exception) as exception_info:
Expand All @@ -111,6 +139,7 @@ def test_post_raw_returns_error_response(
assert api_error.error_code == expected_response_body["error"]["code"]
assert api_error.detail == expected_response_body["error"]["detail"]
assert api_error.docs_url == expected_response_body["error"]["documentation_url"]
assert api_error.retry_after == expected_retry_after

if "errors" in expected_response_body["error"]:
assert len(api_error.field_errors) == len(expected_response_body["error"]["errors"])
Expand Down