Skip to content

Commit 562f219

Browse files
authored
feat(customers): add customer.* webhook events and customer_id list filters (#52)
* feat(customers): add customer.* webhook events and customer_id list filters The deployed API already accepts these; this is additive and safe against production today. - Add customer.new/customer.update/customer.delete to the WebhookEvents Literal, and fold the previously orphaned receiver.delete (from src/blindpay/types.py, disconnected from WebhookEvents and never wired up) into the same Literal alongside it. - Mark receiver.new/receiver.update/receiver.delete deprecated with a comment pointing at the same changelog post used by the receivers namespace deprecation warning in client.py. They are not removed: receiver.* webhooks are still dual-emitted by the deployed API. - Retire the stray, publicly exported types.WebhookEvent alias now that receiver.delete lives in WebhookEvents. - Add customer_id as an optional list filter on ListPayinsInput and ListPayoutsInput, alongside the existing receiver_id (not removed). GET /v1/instances/{id}/payins and /payouts already accept customer_id as a query filter in the deployed API spec. - Update the README quickstart example to use blindpay.customers.get() instead of the runtime-deprecated blindpay.receivers.get(). Deliberately excluded (wave 2, needs blindpay-v2 PR #1799 deployed first): the remaining 11 receiver_* -> customer_* field renames, and removal of receiver.* from the webhook enum. Shipping those today would break every SDK user still on the deployed field names. Claude-Session: https://claude.ai/code/session_01F1stiNzuNtJXoXtiW9ZCbs * fix: keep the WebhookEvent export so this release stays additive The branch had deleted the public `WebhookEvent` alias from types.py and dropped it from __init__.py's imports and __all__. It is dead inside the repo, but it is an exported symbol, so `from blindpay import WebhookEvent` would break on what is otherwise a purely additive minor release. Restored with its original value and a deprecation note pointing at WebhookEvents, which is the alias that actually lists every event. Removal belongs in the next major, alongside dropping the receiver.* members. Claude-Session: https://claude.ai/code/session_01F1stiNzuNtJXoXtiW9ZCbs * fix(customers): align response field names with the deployed API The customer.* rename shims declared customer_id on nested owners, limit-increase requests, blockchain wallets, and offramp wallets. The deployed API still sends receiver_id in all four places (server-side customers rename is not deployed yet). Since these are TypedDicts, a consumer indexing the declared key gets a KeyError today. Verified against apps/api/openapi.json: ReceiverOut.owners[], required receiver_id on BlockchainWalletOut, OfframpWallet, and GetReceiverLimitIncreaseOut. Updated the wallet test fixtures to match and added receiver_id assertions so a regression of this exact bug fails the suite. bank_accounts.py's owners field is untouched: the deployed BankAccountOut has neither receiver_id nor customer_id, so that is a separate pre-existing type-accuracy issue, not this bug. Claude-Session: https://claude.ai/code/session_01F1stiNzuNtJXoXtiW9ZCbs * fix(tests): narrow Optional data before subscripting in wallet list assertions pyright flagged reportOptionalSubscript on response["data"][0] since BlindpayApiResponse["data"] is Optional. Add the same is-not-None assert already used elsewhere in the suite to narrow the type. Claude-Session: https://claude.ai/code/session_01F1stiNzuNtJXoXtiW9ZCbs * fix(customers): declare customer_id where the middleware alias sends it Blockchain wallets, offramp wallets and limit-increase responses go through addCustomerIdMiddleware, which adds customer_id wherever receiver_id is present today, and both are required in the post-#1799 spec. Revert the earlier reversion to receiver_id for those three. Owner is different: it's a nested owners[] element, which the middleware does not recurse into, so customer_id is not sent today but will be after #1799. Declare both keys as NotRequired so neither shape breaks. Claude-Session: https://claude.ai/code/session_01F1stiNzuNtJXoXtiW9ZCbs
1 parent 4836206 commit 562f219

9 files changed

Lines changed: 30 additions & 7 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ All API methods return a response dictionary with either `data` or `error`:
2727
instance_id="your_instance_id_here"
2828
)
2929

30-
response = await blindpay.receivers.get("receiver-id")
30+
response = await blindpay.customers.get("customer-id")
3131

3232
if response['error']:
3333
print(f"Error: {response['error']['message']}")
3434
return
3535

36-
receiver = response['data']
37-
print(f"Receiver: {receiver}")
36+
customer = response['data']
37+
print(f"Customer: {customer}")
3838
```
3939

4040
## Types

‎src/blindpay/resources/customers/customers.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import List, Optional, Union
22
from urllib.parse import urlencode
33

4-
from typing_extensions import Literal, TypedDict
4+
from typing_extensions import Literal, NotRequired, TypedDict
55

66
from ..._internal.api_client import InternalApiClient, InternalApiClientSync
77
from ...types import (
@@ -302,7 +302,10 @@ class TransactionLimit(TypedDict):
302302
class Owner(TypedDict):
303303
id: str
304304
instance_id: str
305-
customer_id: str
305+
# Not yet added by the response middleware for nested owners; both keys are
306+
# optional until the API sends customer_id here directly (post-#1799).
307+
receiver_id: NotRequired[str]
308+
customer_id: NotRequired[str]
306309
role: OwnerRole
307310
first_name: str
308311
last_name: str

‎src/blindpay/resources/payins/payins.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class Payin(TypedDict):
103103
class ListPayinsInput(PaginationParams):
104104
status: Optional[TransactionStatus]
105105
receiver_id: Optional[str]
106+
customer_id: Optional[str]
106107

107108

108109
class ListPayinsResponse(TypedDict):

‎src/blindpay/resources/payouts/payouts.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class Payout(TypedDict):
103103

104104
class ListPayoutsInput(PaginationParams, total=False):
105105
receiver_id: str
106+
customer_id: str
106107

107108

108109
class ListPayoutsResponse(TypedDict):

‎src/blindpay/resources/webhooks/webhooks.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,18 @@
66
from ...types import BlindpayApiResponse
77

88
WebhookEvents = Literal[
9+
# deprecated: use "customer.new" instead. See
10+
# https://www.blindpay.com/changelog/2026-06-04-customers-rename
911
"receiver.new",
12+
# deprecated: use "customer.update" instead. See
13+
# https://www.blindpay.com/changelog/2026-06-04-customers-rename
1014
"receiver.update",
15+
# deprecated: use "customer.delete" instead. See
16+
# https://www.blindpay.com/changelog/2026-06-04-customers-rename
17+
"receiver.delete",
18+
"customer.new",
19+
"customer.update",
20+
"customer.delete",
1121
"bankAccount.new",
1222
"payout.new",
1323
"payout.update",

‎src/blindpay/types.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,5 +408,7 @@ class TrackingPartnerFee(TypedDict):
408408

409409
ManualExecutionStatus = Literal["failed"]
410410

411-
411+
# Deprecated: use WebhookEvents from blindpay.resources.webhooks, which lists every
412+
# event. Kept exported so this stays an additive release; scheduled for removal in the
413+
# next major.
412414
WebhookEvent = Literal["receiver.delete"]

‎tests/resources/test_blockchain_wallets.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ async def test_list_blockchain_wallets(self):
4646

4747
assert response["error"] is None
4848
assert response["data"] == mocked_wallets
49+
assert response["data"] is not None
50+
assert response["data"][0]["customer_id"] == "re_000000000000"
4951
mock_request.assert_called_once_with(
5052
"GET", "/instances/in_000000000000/customers/re_000000000000/blockchain-wallets"
5153
)
@@ -326,6 +328,8 @@ def test_list_blockchain_wallets(self):
326328

327329
assert response["error"] is None
328330
assert response["data"] == mocked_wallets
331+
assert response["data"] is not None
332+
assert response["data"][0]["customer_id"] == "re_000000000000"
329333
mock_request.assert_called_once_with(
330334
"GET", "/instances/in_000000000000/customers/re_000000000000/blockchain-wallets"
331335
)

‎tests/resources/test_offramp_wallets.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ async def test_list_offramp_wallets(self):
3838

3939
assert response["error"] is None
4040
assert response["data"] == mocked_offramp_wallets
41+
assert response["data"] is not None
42+
assert response["data"][0]["customer_id"] == "re_000000000000"
4143
mock_request.assert_called_once_with(
4244
"GET",
4345
"/instances/in_000000000000/customers/re_000000000000/bank-accounts/ba_000000000000/offramp-wallets",

‎uv.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)