From 5e247f769467ca76169066d1012544207365b209 Mon Sep 17 00:00:00 2001 From: Christian Piedrahita Date: Tue, 4 Aug 2026 18:01:15 -0400 Subject: [PATCH 1/7] Add Secrets, ForwardingRequests, and Teams MLE public key resources Implements the resources the API docs already show Python examples for: - method.secrets: create, retrieve, list, delete - method.forwarding_requests: create, retrieve - method.teams.mle.public_keys: create, list, retrieve, delete Response types follow octo's public types (ForwardingRequest carries status_history and no updated_at; headers/body are required on create, matching the API's inbound validation). Integration tests follow the existing live dev-API pattern. The ForwardingRequest create tests are skipped pending a whitelisted destination URL and payment instrument (see skip reasons). Co-Authored-By: Claude Fable 5 --- method/method.py | 9 ++ .../ForwardingRequests/ForwardingRequest.py | 68 ++++++++++++ .../resources/ForwardingRequests/__init__.py | 1 + method/resources/Secrets/Secret.py | 44 ++++++++ method/resources/Secrets/__init__.py | 1 + method/resources/Teams/Team.py | 78 +++++++++++++ method/resources/Teams/__init__.py | 1 + method/resources/__init__.py | 5 +- test/resources/ForwardingRequest_test.py | 105 ++++++++++++++++++ test/resources/Secret_test.py | 64 +++++++++++ test/resources/TeamMLEPublicKey_test.py | 82 ++++++++++++++ 11 files changed, 457 insertions(+), 1 deletion(-) create mode 100644 method/resources/ForwardingRequests/ForwardingRequest.py create mode 100644 method/resources/ForwardingRequests/__init__.py create mode 100644 method/resources/Secrets/Secret.py create mode 100644 method/resources/Secrets/__init__.py create mode 100644 method/resources/Teams/Team.py create mode 100644 method/resources/Teams/__init__.py create mode 100644 test/resources/ForwardingRequest_test.py create mode 100644 test/resources/Secret_test.py create mode 100644 test/resources/TeamMLEPublicKey_test.py diff --git a/method/method.py b/method/method.py index 42b16c8..cc1f068 100644 --- a/method/method.py +++ b/method/method.py @@ -12,6 +12,9 @@ from method.resources.Events import EventResource from method.resources.CardProduct import CardProductResource from method.resources.Opal import OpalResource +from method.resources.Secrets import SecretResource +from method.resources.ForwardingRequests import ForwardingRequestResource +from method.resources.Teams import TeamResource class Method: accounts: AccountResource @@ -26,6 +29,9 @@ class Method: simulate: SimulateResource card_products: CardProductResource opal: OpalResource + secrets: SecretResource + forwarding_requests: ForwardingRequestResource + teams: TeamResource def __init__(self, opts: ConfigurationOpts = None, **kwargs: ConfigurationOpts): _opts: ConfigurationOpts = {**(opts or {}), **kwargs} # type: ignore @@ -43,6 +49,9 @@ def __init__(self, opts: ConfigurationOpts = None, **kwargs: ConfigurationOpts): self.simulate = SimulateResource(config) self.card_products = CardProductResource(config) self.opal = OpalResource(config) + self.secrets = SecretResource(config) + self.forwarding_requests = ForwardingRequestResource(config) + self.teams = TeamResource(config) def ping(self) -> MethodResponse[PingResponse]: return self.healthcheck.retrieve() diff --git a/method/resources/ForwardingRequests/ForwardingRequest.py b/method/resources/ForwardingRequests/ForwardingRequest.py new file mode 100644 index 0000000..11dbc0d --- /dev/null +++ b/method/resources/ForwardingRequests/ForwardingRequest.py @@ -0,0 +1,68 @@ +from typing import TypedDict, Optional, List, Dict, Any, Literal + +from method.resource import MethodResponse, Resource, RequestOpts +from method.configuration import Configuration + + +ForwardingRequestStatusesLiterals = Literal[ + 'completed', + 'failed' +] + + +ForwardingRequestMethodsLiterals = Literal[ + 'GET', + 'POST', + 'PUT', + 'PATCH', + 'DELETE' +] + + +class ForwardingRequestDetail(TypedDict): + url: str + method: ForwardingRequestMethodsLiterals + headers: Dict[str, str] + body: str + + +class ForwardingResponseDetail(TypedDict): + status_code: Optional[int] + headers: Dict[str, str] + body: Any + + +class ForwardingRequestStatusHistoryItem(TypedDict): + status: str + message: Optional[str] + + +class ForwardingRequest(TypedDict): + id: str + bindings: Dict[str, str] + request: ForwardingRequestDetail + response: ForwardingResponseDetail + duration_ms: int + status: ForwardingRequestStatusesLiterals + status_history: List[ForwardingRequestStatusHistoryItem] + created_at: str + + +class ForwardingRequestCreateOpts(TypedDict): + bindings: Dict[str, str] + url: str + method: ForwardingRequestMethodsLiterals + headers: Dict[str, str] + body: str + metadata: Optional[Dict[str, Any]] + + +class ForwardingRequestResource(Resource): + def __init__(self, config: Configuration): + super(ForwardingRequestResource, self).__init__(config.add_path('forwarding_requests')) + + def create(self, opts: ForwardingRequestCreateOpts, request_opts: Optional[RequestOpts] = None) -> MethodResponse[ForwardingRequest]: + return super(ForwardingRequestResource, self)._create(opts, request_opts=request_opts) + + def retrieve(self, _id: str) -> MethodResponse[ForwardingRequest]: + return super(ForwardingRequestResource, self)._get_with_id(_id) diff --git a/method/resources/ForwardingRequests/__init__.py b/method/resources/ForwardingRequests/__init__.py new file mode 100644 index 0000000..9e1cc63 --- /dev/null +++ b/method/resources/ForwardingRequests/__init__.py @@ -0,0 +1 @@ +from method.resources.ForwardingRequests.ForwardingRequest import ForwardingRequest, ForwardingRequestResource diff --git a/method/resources/Secrets/Secret.py b/method/resources/Secrets/Secret.py new file mode 100644 index 0000000..aa21a06 --- /dev/null +++ b/method/resources/Secrets/Secret.py @@ -0,0 +1,44 @@ +from typing import TypedDict, Optional, List, Dict, Any, Literal + +from method.resource import MethodResponse, Resource, RequestOpts, ResourceListOpts +from method.configuration import Configuration + + +SecretStatusesLiterals = Literal[ + 'active', + 'deleted' +] + + +class Secret(TypedDict): + id: str + metadata: Optional[Dict[str, Any]] + status: SecretStatusesLiterals + created_at: str + updated_at: str + + +class SecretCreateOpts(TypedDict): + value: str + metadata: Optional[Dict[str, Any]] + + +class SecretListOpts(ResourceListOpts): + pass + + +class SecretResource(Resource): + def __init__(self, config: Configuration): + super(SecretResource, self).__init__(config.add_path('secrets')) + + def create(self, opts: SecretCreateOpts, request_opts: Optional[RequestOpts] = None) -> MethodResponse[Secret]: + return super(SecretResource, self)._create(opts, request_opts=request_opts) + + def retrieve(self, _id: str) -> MethodResponse[Secret]: + return super(SecretResource, self)._get_with_id(_id) + + def list(self, params: Optional[SecretListOpts] = None) -> MethodResponse[List[Secret]]: + return super(SecretResource, self)._list(params) + + def delete(self, _id: str) -> MethodResponse[None]: + return super(SecretResource, self)._delete(_id) diff --git a/method/resources/Secrets/__init__.py b/method/resources/Secrets/__init__.py new file mode 100644 index 0000000..097c6ab --- /dev/null +++ b/method/resources/Secrets/__init__.py @@ -0,0 +1 @@ +from method.resources.Secrets.Secret import Secret, SecretResource diff --git a/method/resources/Teams/Team.py b/method/resources/Teams/Team.py new file mode 100644 index 0000000..1ec6487 --- /dev/null +++ b/method/resources/Teams/Team.py @@ -0,0 +1,78 @@ +from typing import TypedDict, Optional, List, Literal + +from method.resource import MethodResponse, Resource, RequestOpts +from method.configuration import Configuration + + +MLEPublicKeyTypesLiterals = Literal[ + 'direct', + 'well_known' +] + + +MLEPublicKeyStatusesLiterals = Literal[ + 'active', + 'disabled' +] + + +class JWK(TypedDict): + kid: Optional[str] + kty: str + alg: Optional[str] + use: Optional[str] + n: str + e: str + + +class MLEPublicKey(TypedDict): + id: str + type: MLEPublicKeyTypesLiterals + jwk: Optional[JWK] + well_known_endpoint: Optional[str] + status: MLEPublicKeyStatusesLiterals + contact: str + created_at: str + updated_at: str + + +class MLEPublicKeyCreateOpts(TypedDict): + type: MLEPublicKeyTypesLiterals + contact: str + jwk: Optional[JWK] + well_known_endpoint: Optional[str] + + +class TeamMLEPublicKeysResource(Resource): + def __init__(self, config: Configuration): + super(TeamMLEPublicKeysResource, self).__init__(config.add_path('public_keys')) + + def create(self, opts: MLEPublicKeyCreateOpts, request_opts: Optional[RequestOpts] = None) -> MethodResponse[MLEPublicKey]: + return super(TeamMLEPublicKeysResource, self)._create(opts, request_opts=request_opts) + + def list(self) -> MethodResponse[List[MLEPublicKey]]: + return super(TeamMLEPublicKeysResource, self)._list(None) + + def retrieve(self, _id: str) -> MethodResponse[MLEPublicKey]: + return super(TeamMLEPublicKeysResource, self)._get_with_id(_id) + + def delete(self, _id: str) -> MethodResponse[MLEPublicKey]: + return super(TeamMLEPublicKeysResource, self)._delete(_id) + + +class TeamMLEResource(Resource): + public_keys: TeamMLEPublicKeysResource + + def __init__(self, config: Configuration): + _config = config.add_path('mle') + super(TeamMLEResource, self).__init__(_config) + self.public_keys = TeamMLEPublicKeysResource(_config) + + +class TeamResource(Resource): + mle: TeamMLEResource + + def __init__(self, config: Configuration): + _config = config.add_path('teams') + super(TeamResource, self).__init__(_config) + self.mle = TeamMLEResource(_config) diff --git a/method/resources/Teams/__init__.py b/method/resources/Teams/__init__.py new file mode 100644 index 0000000..8203a5f --- /dev/null +++ b/method/resources/Teams/__init__.py @@ -0,0 +1 @@ +from method.resources.Teams.Team import MLEPublicKey, TeamResource, TeamMLEResource, TeamMLEPublicKeysResource diff --git a/method/resources/__init__.py b/method/resources/__init__.py index 30baf82..1f96e5e 100644 --- a/method/resources/__init__.py +++ b/method/resources/__init__.py @@ -14,4 +14,7 @@ from method.resources.Merchant import Merchant, MerchantProviderIds, MerchantResource from method.resources.Report import Report, ReportCreateOpts, ReportResource from method.resources.Webhook import Webhook, WebhookCreateOpts, WebhookResource -from method.resources.Events.Event import Event, EventResource \ No newline at end of file +from method.resources.Events.Event import Event, EventResource +from method.resources.Secrets.Secret import Secret, SecretCreateOpts, SecretResource +from method.resources.ForwardingRequests.ForwardingRequest import ForwardingRequest, ForwardingRequestCreateOpts, ForwardingRequestResource +from method.resources.Teams.Team import MLEPublicKey, MLEPublicKeyCreateOpts, TeamResource, TeamMLEResource, TeamMLEPublicKeysResource \ No newline at end of file diff --git a/test/resources/ForwardingRequest_test.py b/test/resources/ForwardingRequest_test.py new file mode 100644 index 0000000..3efc904 --- /dev/null +++ b/test/resources/ForwardingRequest_test.py @@ -0,0 +1,105 @@ +import os +import pytest +from method import Method +from dotenv import load_dotenv + +load_dotenv() + +API_KEY = os.getenv('API_KEY') + +method = Method(env='dev', api_key=API_KEY) + +# Forwarding requests are executed synchronously against a destination URL that must be +# whitelisted for the team (e.g. Fidel, Tabapay, Recurly), and their bindings must reference +# real resources owned by the team (e.g. a card payment instrument, a secret, an entity). +# A maintainer must provide the following to exercise these tests, then remove the skips: +# FWD_REQUEST_URL - a destination URL whitelisted for the team +# FWD_REQUEST_PMT_INST_ID - an active card payment instrument id (pmt_inst_...) owned by the team +FWD_REQUEST_URL = os.getenv('FWD_REQUEST_URL') +FWD_REQUEST_PMT_INST_ID = os.getenv('FWD_REQUEST_PMT_INST_ID') + +SKIP_REASON = ( + 'Requires a whitelisted destination URL (FWD_REQUEST_URL) and a real card payment ' + 'instrument (FWD_REQUEST_PMT_INST_ID) provisioned for the team in the dev environment.' +) + +forwarding_requests_create_response = None +forwarding_requests_retrieve_response = None + +@pytest.fixture(scope='module') +def setup(): + holder_1_response = method.entities.create({ + 'type': 'individual', + 'individual': { + 'first_name': 'Kevin', + 'last_name': 'Doyle', + 'dob': '1930-03-11', + 'email': 'kevin.doyle@gmail.com', + 'phone': '+15121231111', + } + }) + + secret_1_response = method.secrets.create({ + 'value': 'test_secret_value' + }) + + return { + 'holder_1_id': holder_1_response['id'], + 'secret_1_id': secret_1_response['id'], + } + + +@pytest.mark.skip(reason=SKIP_REASON) +def test_create_forwarding_request(setup): + global forwarding_requests_create_response + + forwarding_requests_create_response = method.forwarding_requests.create({ + 'bindings': { + 'card': FWD_REQUEST_PMT_INST_ID, + 'individual': setup['holder_1_id'], + 'api_key': setup['secret_1_id'], + }, + 'url': FWD_REQUEST_URL, + 'method': 'POST', + 'headers': { + 'Authorization': 'Bearer {{ api_key.value }}' + }, + 'body': '{ "card": { "accountNumber": "{{ card.number }}" }, "owner": { "firstName": "{{ individual.first_name }}", "lastName": "{{ individual.last_name }}" } }' + }) + + expect_results = { + 'id': forwarding_requests_create_response['id'], + 'bindings': { + 'card': FWD_REQUEST_PMT_INST_ID, + 'individual': setup['holder_1_id'], + 'api_key': setup['secret_1_id'], + }, + 'request': forwarding_requests_create_response['request'], + 'response': forwarding_requests_create_response['response'], + 'duration_ms': forwarding_requests_create_response['duration_ms'], + 'status': 'completed', + 'status_history': forwarding_requests_create_response['status_history'], + 'created_at': forwarding_requests_create_response['created_at'], + } + + assert forwarding_requests_create_response == expect_results + + +@pytest.mark.skip(reason=SKIP_REASON) +def test_retrieve_forwarding_request(setup): + global forwarding_requests_retrieve_response + + forwarding_requests_retrieve_response = method.forwarding_requests.retrieve(forwarding_requests_create_response['id']) + + expect_results = { + 'id': forwarding_requests_create_response['id'], + 'bindings': forwarding_requests_create_response['bindings'], + 'request': forwarding_requests_retrieve_response['request'], + 'response': forwarding_requests_retrieve_response['response'], + 'duration_ms': forwarding_requests_retrieve_response['duration_ms'], + 'status': forwarding_requests_retrieve_response['status'], + 'status_history': forwarding_requests_retrieve_response['status_history'], + 'created_at': forwarding_requests_retrieve_response['created_at'], + } + + assert forwarding_requests_retrieve_response == expect_results diff --git a/test/resources/Secret_test.py b/test/resources/Secret_test.py new file mode 100644 index 0000000..2628414 --- /dev/null +++ b/test/resources/Secret_test.py @@ -0,0 +1,64 @@ +import os +from method import Method +from dotenv import load_dotenv + +load_dotenv() + +API_KEY = os.getenv('API_KEY') + +method = Method(env='dev', api_key=API_KEY) + +secrets_create_response = None +secrets_retrieve_response = None +secrets_list_response = None +secrets_delete_response = None + +def test_create_secret(): + global secrets_create_response + + secrets_create_response = method.secrets.create({ + 'value': 'test_secret_value' + }) + + expect_results = { + 'id': secrets_create_response['id'], + 'metadata': None, + 'status': 'active', + 'created_at': secrets_create_response['created_at'], + 'updated_at': secrets_create_response['updated_at'], + } + + assert secrets_create_response == expect_results + + +def test_retrieve_secret(): + global secrets_retrieve_response + + secrets_retrieve_response = method.secrets.retrieve(secrets_create_response['id']) + + expect_results = { + 'id': secrets_create_response['id'], + 'metadata': None, + 'status': 'active', + 'created_at': secrets_retrieve_response['created_at'], + 'updated_at': secrets_retrieve_response['updated_at'], + } + + assert secrets_retrieve_response == expect_results + + +def test_list_secrets(): + global secrets_list_response + + secrets_list_response = method.secrets.list() + secret_ids = [secret['id'] for secret in secrets_list_response] + + assert secrets_create_response['id'] in secret_ids + + +def test_delete_secret(): + global secrets_delete_response + + secrets_delete_response = method.secrets.delete(secrets_create_response['id']) + + assert secrets_delete_response == None diff --git a/test/resources/TeamMLEPublicKey_test.py b/test/resources/TeamMLEPublicKey_test.py new file mode 100644 index 0000000..1111279 --- /dev/null +++ b/test/resources/TeamMLEPublicKey_test.py @@ -0,0 +1,82 @@ +import os +import uuid +from method import Method +from dotenv import load_dotenv + +load_dotenv() + +API_KEY = os.getenv('API_KEY') + +method = Method(env='dev', api_key=API_KEY) + +test_jwk = { + 'kid': str(uuid.uuid4()), + 'kty': 'RSA', + 'alg': 'RS256', + 'use': 'enc', + 'n': 'x9hKPiAZKzHhAZx670NMvnvI0ZaEa1I92XsQklLORGVqqECy3oA7In8tkb0FEI2V2yJMZhMkf-4EbsTPZu_D7Hqo3E6fHR0FNd0gocpEy5fBf5at6o92ueVmYiDiXsgxFHZzhEo40a26diRBkzzxYpjxZNtvheQiM34n25kSqvJ3sacIguQs4erqgWl2YR8l1HYIX5_9n3wQ3cuU4a0fcHoLtVmD4fymZ1kiESUiU6qkw-XkYn0BZD3TwTbStQrkXDoFt9D7L7-PLCU5Nmqval5RtI2i4q_uks8t9Hg9YjrM3_FnulT18YiLJ0aGUTgx-qaNoJy5OLGfIg0cfhFrvQ', + 'e': 'AQAB' +} + +public_keys_create_response = None +public_keys_retrieve_response = None +public_keys_list_response = None +public_keys_delete_response = None + +def test_create_mle_public_key(): + global public_keys_create_response + + public_keys_create_response = method.teams.mle.public_keys.create({ + 'type': 'direct', + 'contact': 'engineering@methodfi.com', + 'jwk': test_jwk + }) + + expect_results = { + 'id': public_keys_create_response['id'], + 'type': 'direct', + 'jwk': public_keys_create_response['jwk'], + 'well_known_endpoint': None, + 'status': 'active', + 'contact': 'engineering@methodfi.com', + 'created_at': public_keys_create_response['created_at'], + 'updated_at': public_keys_create_response['updated_at'], + } + + assert public_keys_create_response == expect_results + + +def test_list_mle_public_keys(): + global public_keys_list_response + + public_keys_list_response = method.teams.mle.public_keys.list() + public_key_ids = [public_key['id'] for public_key in public_keys_list_response] + + assert public_keys_create_response['id'] in public_key_ids + + +def test_retrieve_mle_public_key(): + global public_keys_retrieve_response + + public_keys_retrieve_response = method.teams.mle.public_keys.retrieve(public_keys_create_response['id']) + + expect_results = { + 'id': public_keys_create_response['id'], + 'type': 'direct', + 'jwk': public_keys_retrieve_response['jwk'], + 'well_known_endpoint': None, + 'status': 'active', + 'contact': 'engineering@methodfi.com', + 'created_at': public_keys_retrieve_response['created_at'], + 'updated_at': public_keys_retrieve_response['updated_at'], + } + + assert public_keys_retrieve_response == expect_results + + +def test_delete_mle_public_key(): + global public_keys_delete_response + + public_keys_delete_response = method.teams.mle.public_keys.delete(public_keys_create_response['id']) + + assert public_keys_delete_response['status'] == 'disabled' From 395b425fea239739386dfcb1768dcc0546e552b2 Mon Sep 17 00:00:00 2001 From: Christian Piedrahita Date: Wed, 5 Aug 2026 10:29:43 -0400 Subject: [PATCH 2/7] Add ManagedAccounts, complete Teams, and update account subscription types Closes out the remaining HIGH priority SDK gaps: - method.managed_accounts: list, retrieve, and per-account transactions (field shapes follow octo's public types; the node SDK's stale shapes were not carried over) - method.teams: list (GET /teams returns sub-teams), create, and update_encryption_key (opts match octo's validators: create requires name/legal_name/ein/contacts; encryption key takes an RSA modulus and exponent pair or a certificate) - Account subscription types gain attribute, payment_instrument.card, and payment_instrument.network_token (connect and credit_score are entity-level subscriptions and already exist on Entities) Team create and encryption key tests are skip-marked: both mutate shared dev-environment state. Co-Authored-By: Claude Fable 5 --- method/method.py | 3 + method/resources/Accounts/Subscriptions.py | 10 +++ method/resources/Accounts/Types.py | 3 + .../ManagedAccounts/ManagedAccount.py | 55 +++++++++++++ method/resources/ManagedAccounts/__init__.py | 1 + method/resources/Teams/Team.py | 76 +++++++++++++++++- method/resources/Teams/__init__.py | 3 +- method/resources/__init__.py | 5 +- test/resources/ManagedAccount_test.py | 63 +++++++++++++++ test/resources/Team_test.py | 77 +++++++++++++++++++ 10 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 method/resources/ManagedAccounts/ManagedAccount.py create mode 100644 method/resources/ManagedAccounts/__init__.py create mode 100644 test/resources/ManagedAccount_test.py create mode 100644 test/resources/Team_test.py diff --git a/method/method.py b/method/method.py index cc1f068..6e2510f 100644 --- a/method/method.py +++ b/method/method.py @@ -15,6 +15,7 @@ from method.resources.Secrets import SecretResource from method.resources.ForwardingRequests import ForwardingRequestResource from method.resources.Teams import TeamResource +from method.resources.ManagedAccounts import ManagedAccountResource class Method: accounts: AccountResource @@ -32,6 +33,7 @@ class Method: secrets: SecretResource forwarding_requests: ForwardingRequestResource teams: TeamResource + managed_accounts: ManagedAccountResource def __init__(self, opts: ConfigurationOpts = None, **kwargs: ConfigurationOpts): _opts: ConfigurationOpts = {**(opts or {}), **kwargs} # type: ignore @@ -52,6 +54,7 @@ def __init__(self, opts: ConfigurationOpts = None, **kwargs: ConfigurationOpts): self.secrets = SecretResource(config) self.forwarding_requests = ForwardingRequestResource(config) self.teams = TeamResource(config) + self.managed_accounts = ManagedAccountResource(config) def ping(self) -> MethodResponse[PingResponse]: return self.healthcheck.retrieve() diff --git a/method/resources/Accounts/Subscriptions.py b/method/resources/Accounts/Subscriptions.py index 9bb1d2c..8cbe543 100644 --- a/method/resources/Accounts/Subscriptions.py +++ b/method/resources/Accounts/Subscriptions.py @@ -5,6 +5,11 @@ AccountSubscriptionTypesLiterals = Literal[ + 'attribute', + 'card_brand', + 'payment_instrument', + 'payment_instrument.card', + 'payment_instrument.network_token', 'transaction', 'update', 'update.snapshot' @@ -21,6 +26,11 @@ class AccountSubscription(TypedDict): AccountSubscriptionsResponse = TypedDict('AccountSubscriptionsResponse', { + 'attribute': Optional[AccountSubscription], + 'card_brand': Optional[AccountSubscription], + 'payment_instrument': Optional[AccountSubscription], + 'payment_instrument.card': Optional[AccountSubscription], + 'payment_instrument.network_token': Optional[AccountSubscription], 'transaction': Optional[AccountSubscription], 'update': Optional[AccountSubscription], 'update.snapshot': Optional[AccountSubscription] diff --git a/method/resources/Accounts/Types.py b/method/resources/Accounts/Types.py index 6e27c65..b9e54c2 100644 --- a/method/resources/Accounts/Types.py +++ b/method/resources/Accounts/Types.py @@ -28,8 +28,11 @@ AccountSubscriptionTypesLiterals = Literal[ + 'attribute', 'card_brand', 'payment_instrument', + 'payment_instrument.card', + 'payment_instrument.network_token', 'transaction', 'update', 'update.snapshot' diff --git a/method/resources/ManagedAccounts/ManagedAccount.py b/method/resources/ManagedAccounts/ManagedAccount.py new file mode 100644 index 0000000..cde4e9e --- /dev/null +++ b/method/resources/ManagedAccounts/ManagedAccount.py @@ -0,0 +1,55 @@ +from typing import TypedDict, Optional, List + +from method.resource import MethodResponse, Resource +from method.configuration import Configuration + + +class ManagedAccount(TypedDict): + id: str + routing: str + number: str + current_balance: int + available_balance: int + + +class ManagedAccountTransaction(TypedDict): + id: str + description: str + date: str + amount: int + + +class ManagedAccountTransactionListOpts(TypedDict): + page_limit: Optional[int] + from_date: Optional[str] + to_date: Optional[str] + page_cursor: Optional[str] + + +class ManagedAccountTransactionsResource(Resource): + def __init__(self, config: Configuration): + super(ManagedAccountTransactionsResource, self).__init__(config.add_path('transactions')) + + def list(self, params: Optional[ManagedAccountTransactionListOpts] = None) -> MethodResponse[List[ManagedAccountTransaction]]: + return super(ManagedAccountTransactionsResource, self)._list(params) + + +class ManagedAccountSubResources: + transactions: ManagedAccountTransactionsResource + + def __init__(self, _id: str, config: Configuration): + self.transactions = ManagedAccountTransactionsResource(config.add_path(_id)) + + +class ManagedAccountResource(Resource): + def __init__(self, config: Configuration): + super(ManagedAccountResource, self).__init__(config.add_path('managed_accounts')) + + def __call__(self, macc_id: str) -> ManagedAccountSubResources: + return ManagedAccountSubResources(macc_id, self.config) + + def list(self) -> MethodResponse[List[ManagedAccount]]: + return super(ManagedAccountResource, self)._list(None) + + def retrieve(self, macc_id: str) -> MethodResponse[ManagedAccount]: + return super(ManagedAccountResource, self)._get_with_id(macc_id) diff --git a/method/resources/ManagedAccounts/__init__.py b/method/resources/ManagedAccounts/__init__.py new file mode 100644 index 0000000..90ae1c2 --- /dev/null +++ b/method/resources/ManagedAccounts/__init__.py @@ -0,0 +1 @@ +from method.resources.ManagedAccounts.ManagedAccount import ManagedAccount, ManagedAccountTransaction, ManagedAccountResource diff --git a/method/resources/Teams/Team.py b/method/resources/Teams/Team.py index 1ec6487..fccfb7c 100644 --- a/method/resources/Teams/Team.py +++ b/method/resources/Teams/Team.py @@ -1,9 +1,74 @@ -from typing import TypedDict, Optional, List, Literal +from typing import TypedDict, Optional, List, Literal, Union from method.resource import MethodResponse, Resource, RequestOpts from method.configuration import Configuration +TeamStatusesLiterals = Literal[ + 'active', + 'verified', + 'disabled', + 'pending_disablement' +] + + +TeamContactTypesLiterals = Literal[ + 'Admin', + 'Billing', + 'Technical' +] + + +class TeamContact(TypedDict): + name: str + email: str + type: TeamContactTypesLiterals + + +class TeamAddress(TypedDict): + line1: str + line2: Optional[str] + city: str + state: str + zipcode: str + country: Optional[str] + + +class Team(TypedDict): + id: str + parent_id: Optional[str] + name: str + legal_name: str + ein: str + contacts: List[TeamContact] + address: TeamAddress + status: TeamStatusesLiterals + logo: Optional[str] + created_at: str + updated_at: str + + +class TeamCreateOpts(TypedDict): + name: str + legal_name: str + ein: str + contacts: List[TeamContact] + address: Optional[TeamAddress] + + +class TeamEncryptionKeyRSAKey(TypedDict): + keyModulus: str + keyExponent: str + + +class TeamEncryptionKeyCertificate(TypedDict): + certificate: str + + +class TeamEncryptionKeyOpts(TypedDict): + encryption_key: Union[TeamEncryptionKeyRSAKey, TeamEncryptionKeyCertificate] + + MLEPublicKeyTypesLiterals = Literal[ 'direct', 'well_known' @@ -76,3 +141,12 @@ def __init__(self, config: Configuration): _config = config.add_path('teams') super(TeamResource, self).__init__(_config) self.mle = TeamMLEResource(_config) + + def list(self) -> MethodResponse[List[Team]]: + return super(TeamResource, self)._list(None) + + def create(self, opts: TeamCreateOpts, request_opts: Optional[RequestOpts] = None) -> MethodResponse[Team]: + return super(TeamResource, self)._create(opts, request_opts=request_opts) + + def update_encryption_key(self, opts: TeamEncryptionKeyOpts) -> MethodResponse[None]: + return super(TeamResource, self)._create_with_sub_path('default_encryption_key', opts) diff --git a/method/resources/Teams/__init__.py b/method/resources/Teams/__init__.py index 8203a5f..3812bb8 100644 --- a/method/resources/Teams/__init__.py +++ b/method/resources/Teams/__init__.py @@ -1 +1,2 @@ -from method.resources.Teams.Team import MLEPublicKey, TeamResource, TeamMLEResource, TeamMLEPublicKeysResource +from method.resources.Teams.Team import Team, TeamCreateOpts, TeamEncryptionKeyOpts, MLEPublicKey, TeamResource, TeamMLEResource, \ + TeamMLEPublicKeysResource diff --git a/method/resources/__init__.py b/method/resources/__init__.py index 1f96e5e..cffc21c 100644 --- a/method/resources/__init__.py +++ b/method/resources/__init__.py @@ -17,4 +17,7 @@ from method.resources.Events.Event import Event, EventResource from method.resources.Secrets.Secret import Secret, SecretCreateOpts, SecretResource from method.resources.ForwardingRequests.ForwardingRequest import ForwardingRequest, ForwardingRequestCreateOpts, ForwardingRequestResource -from method.resources.Teams.Team import MLEPublicKey, MLEPublicKeyCreateOpts, TeamResource, TeamMLEResource, TeamMLEPublicKeysResource \ No newline at end of file +from method.resources.Teams.Team import Team, TeamContact, TeamAddress, TeamCreateOpts, TeamEncryptionKeyOpts, MLEPublicKey, \ + MLEPublicKeyCreateOpts, TeamResource, TeamMLEResource, TeamMLEPublicKeysResource +from method.resources.ManagedAccounts.ManagedAccount import ManagedAccount, ManagedAccountTransaction, \ + ManagedAccountTransactionListOpts, ManagedAccountResource \ No newline at end of file diff --git a/test/resources/ManagedAccount_test.py b/test/resources/ManagedAccount_test.py new file mode 100644 index 0000000..fbb9efc --- /dev/null +++ b/test/resources/ManagedAccount_test.py @@ -0,0 +1,63 @@ +import os +import pytest +from method import Method +from dotenv import load_dotenv + +load_dotenv() + +API_KEY = os.getenv('API_KEY') + +# Managed accounts cannot be created via the public API; they are provisioned per-team by +# Method. If the dev team has none, set MANAGED_ACCOUNT_ID to a macc_... id provisioned for +# the team to exercise the retrieve and transactions tests. +MANAGED_ACCOUNT_ID = os.getenv('MANAGED_ACCOUNT_ID') + +method = Method(env='dev', api_key=API_KEY) + +MANAGED_ACCOUNT_EXPECTED_KEYS = ['id', 'routing', 'number', 'current_balance', 'available_balance'] +MANAGED_ACCOUNT_TRANSACTION_EXPECTED_KEYS = ['id', 'description', 'date', 'amount'] + + +def get_test_managed_account_id(): + if MANAGED_ACCOUNT_ID: + return MANAGED_ACCOUNT_ID + + managed_accounts_list_response = method.managed_accounts.list() + + if len(managed_accounts_list_response) == 0: + pytest.skip('No managed accounts provisioned for the dev team and MANAGED_ACCOUNT_ID is not set.') + + return managed_accounts_list_response[0]['id'] + + +def test_list_managed_accounts(): + managed_accounts_list_response = method.managed_accounts.list() + + assert isinstance(managed_accounts_list_response.to_dict(), list) + + for managed_account in managed_accounts_list_response: + for key in MANAGED_ACCOUNT_EXPECTED_KEYS: + assert key in managed_account + + +def test_retrieve_managed_account(): + macc_id = get_test_managed_account_id() + + managed_accounts_retrieve_response = method.managed_accounts.retrieve(macc_id) + + assert managed_accounts_retrieve_response['id'] == macc_id + + for key in MANAGED_ACCOUNT_EXPECTED_KEYS: + assert key in managed_accounts_retrieve_response + + +def test_list_managed_account_transactions(): + macc_id = get_test_managed_account_id() + + managed_account_transactions_list_response = method.managed_accounts(macc_id).transactions.list() + + assert isinstance(managed_account_transactions_list_response.to_dict(), list) + + for transaction in managed_account_transactions_list_response: + for key in MANAGED_ACCOUNT_TRANSACTION_EXPECTED_KEYS: + assert key in transaction diff --git a/test/resources/Team_test.py b/test/resources/Team_test.py new file mode 100644 index 0000000..c87c3e7 --- /dev/null +++ b/test/resources/Team_test.py @@ -0,0 +1,77 @@ +import os +import pytest +from method import Method +from dotenv import load_dotenv + +load_dotenv() + +API_KEY = os.getenv('API_KEY') + +method = Method(env='dev', api_key=API_KEY) + +TEAM_EXPECTED_KEYS = ['id', 'parent_id', 'name', 'legal_name', 'ein', 'contacts', 'address', 'status', 'created_at', 'updated_at', 'logo'] + +CREATE_SKIP_REASON = ( + 'POST /teams provisions a new sub-team in the shared dev environment, and teams cannot be ' + 'deleted via the public API.' +) + +ENCRYPTION_KEY_SKIP_REASON = ( + 'POST /teams/default_encryption_key replaces the dev team\'s active default encryption key, ' + 'mutating shared dev state for every consumer of the team.' +) + + +def test_list_teams(): + teams_list_response = method.teams.list() + + assert isinstance(teams_list_response.to_dict(), list) + + for team in teams_list_response: + for key in TEAM_EXPECTED_KEYS: + assert key in team + + +@pytest.mark.skip(reason=CREATE_SKIP_REASON) +def test_create_team(): + team_create_response = method.teams.create({ + 'name': 'Test Sub Team', + 'legal_name': 'Test Sub Team LLC', + 'ein': '12-3456789', + 'contacts': [{ + 'name': 'Kevin Doyle', + 'email': 'kevin.doyle@gmail.com', + 'type': 'Admin', + }], + }) + + expect_results = { + 'id': team_create_response['id'], + 'parent_id': team_create_response['parent_id'], + 'name': 'Test Sub Team', + 'legal_name': 'Test Sub Team LLC', + 'ein': '12-3456789', + 'contacts': [{ + 'name': 'Kevin Doyle', + 'email': 'kevin.doyle@gmail.com', + 'type': 'Admin', + }], + 'address': team_create_response['address'], + 'status': team_create_response['status'], + 'logo': team_create_response['logo'], + 'created_at': team_create_response['created_at'], + 'updated_at': team_create_response['updated_at'], + } + + assert team_create_response == expect_results + + +@pytest.mark.skip(reason=ENCRYPTION_KEY_SKIP_REASON) +def test_update_encryption_key(): + update_encryption_key_response = method.teams.update_encryption_key({ + 'encryption_key': { + 'certificate': '-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----', + }, + }) + + assert update_encryption_key_response.to_dict() is None From 92289c51b1462cfabd5aea9e35b2fd1462602c57 Mon Sep 17 00:00:00 2001 From: Christian Piedrahita Date: Wed, 5 Aug 2026 11:14:31 -0400 Subject: [PATCH 3/7] Fix CI failures in Secret and Team MLE public key tests - Secret responses include an error field (null when healthy); add it to the Secret TypedDict and test expectations - The MLE public key endpoint only accepts jwk.alg RSA-OAEP-256; the test JWK used RS256, which the API rejects with a 400 Co-Authored-By: Claude Fable 5 --- method/resources/Secrets/Secret.py | 2 ++ test/resources/Secret_test.py | 2 ++ test/resources/TeamMLEPublicKey_test.py | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/method/resources/Secrets/Secret.py b/method/resources/Secrets/Secret.py index aa21a06..11cfac8 100644 --- a/method/resources/Secrets/Secret.py +++ b/method/resources/Secrets/Secret.py @@ -2,6 +2,7 @@ from method.resource import MethodResponse, Resource, RequestOpts, ResourceListOpts from method.configuration import Configuration +from method.errors import ResourceError SecretStatusesLiterals = Literal[ @@ -14,6 +15,7 @@ class Secret(TypedDict): id: str metadata: Optional[Dict[str, Any]] status: SecretStatusesLiterals + error: Optional[ResourceError] created_at: str updated_at: str diff --git a/test/resources/Secret_test.py b/test/resources/Secret_test.py index 2628414..ead64a3 100644 --- a/test/resources/Secret_test.py +++ b/test/resources/Secret_test.py @@ -24,6 +24,7 @@ def test_create_secret(): 'id': secrets_create_response['id'], 'metadata': None, 'status': 'active', + 'error': None, 'created_at': secrets_create_response['created_at'], 'updated_at': secrets_create_response['updated_at'], } @@ -40,6 +41,7 @@ def test_retrieve_secret(): 'id': secrets_create_response['id'], 'metadata': None, 'status': 'active', + 'error': None, 'created_at': secrets_retrieve_response['created_at'], 'updated_at': secrets_retrieve_response['updated_at'], } diff --git a/test/resources/TeamMLEPublicKey_test.py b/test/resources/TeamMLEPublicKey_test.py index 1111279..9ace022 100644 --- a/test/resources/TeamMLEPublicKey_test.py +++ b/test/resources/TeamMLEPublicKey_test.py @@ -12,7 +12,7 @@ test_jwk = { 'kid': str(uuid.uuid4()), 'kty': 'RSA', - 'alg': 'RS256', + 'alg': 'RSA-OAEP-256', 'use': 'enc', 'n': 'x9hKPiAZKzHhAZx670NMvnvI0ZaEa1I92XsQklLORGVqqECy3oA7In8tkb0FEI2V2yJMZhMkf-4EbsTPZu_D7Hqo3E6fHR0FNd0gocpEy5fBf5at6o92ueVmYiDiXsgxFHZzhEo40a26diRBkzzxYpjxZNtvheQiM34n25kSqvJ3sacIguQs4erqgWl2YR8l1HYIX5_9n3wQ3cuU4a0fcHoLtVmD4fymZ1kiESUiU6qkw-XkYn0BZD3TwTbStQrkXDoFt9D7L7-PLCU5Nmqval5RtI2i4q_uks8t9Hg9YjrM3_FnulT18YiLJ0aGUTgx-qaNoJy5OLGfIg0cfhFrvQ', 'e': 'AQAB' From d77571fd5ea7b8b3457f77283b775301b1fb2d24 Mon Sep 17 00:00:00 2001 From: Christian Piedrahita Date: Tue, 25 Aug 2026 16:58:48 -0400 Subject: [PATCH 4/7] bump method-version header to 2025-12-01 Resolves Codex review comment: the new secrets, forwarding_requests, and teams.mle.public_keys resources are documented under Method-Version 2025-12-01, and the versioning docs pin method-python v2.1.0+ to that version. Also adds the split payment_instrument product names introduced by 2025-12-01 to the Account product literals. Co-Authored-By: Claude Fable 5 --- method/resource.py | 2 +- method/resources/Accounts/Types.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/method/resource.py b/method/resource.py index 37e50b4..b0192ea 100644 --- a/method/resource.py +++ b/method/resource.py @@ -147,7 +147,7 @@ def __init__(self, config: Configuration): 'Authorization': 'Bearer {token}'.format(token=config.api_key), 'Content-Type': 'application/json', 'User-Agent': 'Method-Python/v{version}'.format(version=version('method-python')), - 'method-version': '2025-07-04' + 'method-version': '2025-12-01' }) def _make_request(self, method: str, path: Optional[str] = None, data: Optional[Dict] = None, params: Optional[Dict] = None, headers: Optional[Dict] = None, raw: bool = False, download: bool = False) -> Union[MethodResponse[T], str]: diff --git a/method/resources/Accounts/Types.py b/method/resources/Accounts/Types.py index b9e54c2..6650cd2 100644 --- a/method/resources/Accounts/Types.py +++ b/method/resources/Accounts/Types.py @@ -23,7 +23,10 @@ 'update', 'attribute', 'transaction', - 'payment_instrument' + 'payment_instrument', + 'payment_instrument.card', + 'payment_instrument.inbound_achwire_payment', + 'payment_instrument.network_token' ] From c1a6a20321e73973149e255775bd163f34e562f1 Mon Sep 17 00:00:00 2001 From: Christian Piedrahita Date: Tue, 25 Aug 2026 17:26:08 -0400 Subject: [PATCH 5/7] skip live dev API test modules when API_KEY is not set Addresses Copilot review comments: the five new test modules now carry a module-level pytestmark skip so they are skipped instead of failing with auth/network errors when API_KEY is not configured. Also replaces the loose == None assertion in Secret_test.py with to_dict() is None, which is lint-clean and equivalent since MethodResponse.__eq__ compares the wrapped payload. Co-Authored-By: Claude Fable 5 --- test/resources/ForwardingRequest_test.py | 2 ++ test/resources/ManagedAccount_test.py | 2 ++ test/resources/Secret_test.py | 5 ++++- test/resources/TeamMLEPublicKey_test.py | 3 +++ test/resources/Team_test.py | 2 ++ 5 files changed, 13 insertions(+), 1 deletion(-) diff --git a/test/resources/ForwardingRequest_test.py b/test/resources/ForwardingRequest_test.py index 3efc904..f00282c 100644 --- a/test/resources/ForwardingRequest_test.py +++ b/test/resources/ForwardingRequest_test.py @@ -7,6 +7,8 @@ API_KEY = os.getenv('API_KEY') +pytestmark = pytest.mark.skipif(not API_KEY, reason='API_KEY is not set; skipping live dev API tests.') + method = Method(env='dev', api_key=API_KEY) # Forwarding requests are executed synchronously against a destination URL that must be diff --git a/test/resources/ManagedAccount_test.py b/test/resources/ManagedAccount_test.py index fbb9efc..2ab011d 100644 --- a/test/resources/ManagedAccount_test.py +++ b/test/resources/ManagedAccount_test.py @@ -7,6 +7,8 @@ API_KEY = os.getenv('API_KEY') +pytestmark = pytest.mark.skipif(not API_KEY, reason='API_KEY is not set; skipping live dev API tests.') + # Managed accounts cannot be created via the public API; they are provisioned per-team by # Method. If the dev team has none, set MANAGED_ACCOUNT_ID to a macc_... id provisioned for # the team to exercise the retrieve and transactions tests. diff --git a/test/resources/Secret_test.py b/test/resources/Secret_test.py index ead64a3..02a0412 100644 --- a/test/resources/Secret_test.py +++ b/test/resources/Secret_test.py @@ -1,4 +1,5 @@ import os +import pytest from method import Method from dotenv import load_dotenv @@ -6,6 +7,8 @@ API_KEY = os.getenv('API_KEY') +pytestmark = pytest.mark.skipif(not API_KEY, reason='API_KEY is not set; skipping live dev API tests.') + method = Method(env='dev', api_key=API_KEY) secrets_create_response = None @@ -63,4 +66,4 @@ def test_delete_secret(): secrets_delete_response = method.secrets.delete(secrets_create_response['id']) - assert secrets_delete_response == None + assert secrets_delete_response.to_dict() is None diff --git a/test/resources/TeamMLEPublicKey_test.py b/test/resources/TeamMLEPublicKey_test.py index 9ace022..559769b 100644 --- a/test/resources/TeamMLEPublicKey_test.py +++ b/test/resources/TeamMLEPublicKey_test.py @@ -1,5 +1,6 @@ import os import uuid +import pytest from method import Method from dotenv import load_dotenv @@ -7,6 +8,8 @@ API_KEY = os.getenv('API_KEY') +pytestmark = pytest.mark.skipif(not API_KEY, reason='API_KEY is not set; skipping live dev API tests.') + method = Method(env='dev', api_key=API_KEY) test_jwk = { diff --git a/test/resources/Team_test.py b/test/resources/Team_test.py index c87c3e7..24d6486 100644 --- a/test/resources/Team_test.py +++ b/test/resources/Team_test.py @@ -7,6 +7,8 @@ API_KEY = os.getenv('API_KEY') +pytestmark = pytest.mark.skipif(not API_KEY, reason='API_KEY is not set; skipping live dev API tests.') + method = Method(env='dev', api_key=API_KEY) TEAM_EXPECTED_KEYS = ['id', 'parent_id', 'name', 'legal_name', 'ein', 'contacts', 'address', 'status', 'created_at', 'updated_at', 'logo'] From 3e28060a1b850186675e592b9d27dca8f3c9eeb5 Mon Sep 17 00:00:00 2001 From: Christian Piedrahita Date: Tue, 25 Aug 2026 22:51:52 -0400 Subject: [PATCH 6/7] delete the fixture-created secret in forwarding request test teardown Addresses Copilot review comment: the module fixture now yields and deletes its secret on teardown so un-skipped runs do not leave active secrets in the shared dev environment. The entity created by the same fixture is left in place because entities cannot be deleted via the public API, matching every other test module in the suite. Co-Authored-By: Claude Fable 5 --- test/resources/ForwardingRequest_test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/resources/ForwardingRequest_test.py b/test/resources/ForwardingRequest_test.py index f00282c..8428f99 100644 --- a/test/resources/ForwardingRequest_test.py +++ b/test/resources/ForwardingRequest_test.py @@ -45,11 +45,14 @@ def setup(): 'value': 'test_secret_value' }) - return { + yield { 'holder_1_id': holder_1_response['id'], 'secret_1_id': secret_1_response['id'], } + # Entities cannot be deleted via the public API, so only the secret is cleaned up. + method.secrets.delete(secret_1_response['id']) + @pytest.mark.skip(reason=SKIP_REASON) def test_create_forwarding_request(setup): From 1d0c1fd0f4457c0a4ae562309619126570c487dc Mon Sep 17 00:00:00 2001 From: Christian Piedrahita Date: Wed, 26 Aug 2026 10:28:25 -0400 Subject: [PATCH 7/7] add payment instrument delete and inbound_achwire_payment typing Adds delete() to AccountPaymentInstrumentsResource, matching method-node and DELETE /accounts/{acc_id}/payment_instruments/{pmt_inst_id} in the 2025-12-01 API. Also adds the inbound_achwire_payment type literal and payload TypedDict (account_number, routing_number, reversal_account) and the closed status introduced by the same version. Co-Authored-By: Claude Fable 5 --- method/resources/Accounts/PaymentInstruments.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/method/resources/Accounts/PaymentInstruments.py b/method/resources/Accounts/PaymentInstruments.py index 4e34efd..3f8c931 100644 --- a/method/resources/Accounts/PaymentInstruments.py +++ b/method/resources/Accounts/PaymentInstruments.py @@ -6,7 +6,8 @@ AccountPaymentInstrumentTypesLiterals = Literal[ 'card', - 'network_token' + 'network_token', + 'inbound_achwire_payment' ] class AccountPaymentInstrumentCreateOpts(TypedDict): @@ -20,11 +21,17 @@ class AccountPaymentInstrumentCard(TypedDict): exp_month: int exp_year: int +class AccountPaymentInstrumentInboundACHWirePayment(TypedDict): + account_number: str + routing_number: str + reversal_account: Optional[str] + AccountPaymentInstrumentStatusesLiterals = Literal[ 'completed', 'in_progress', 'pending', - 'failed' + 'failed', + 'closed' ] class AccountPaymentInstrument(TypedDict): @@ -33,6 +40,7 @@ class AccountPaymentInstrument(TypedDict): type: AccountPaymentInstrumentTypesLiterals network_token: Optional[AccountPaymentInstrumentNetworkToken] card: Optional[AccountPaymentInstrumentCard] + inbound_achwire_payment: Optional[AccountPaymentInstrumentInboundACHWirePayment] chargeable: bool status: AccountPaymentInstrumentStatusesLiterals error: Optional[ResourceError] @@ -52,3 +60,7 @@ def list(self, params: Optional[ResourceListOpts] = None) -> MethodResponse[List def create(self, data: AccountPaymentInstrumentCreateOpts) -> MethodResponse[AccountPaymentInstrument]: return super(AccountPaymentInstrumentsResource, self)._create(data) + + # Supported only for payment instruments of type inbound_achwire_payment; returns the closed instrument. + def delete(self, pmt_inst_id: str) -> MethodResponse[AccountPaymentInstrument]: + return super(AccountPaymentInstrumentsResource, self)._delete(pmt_inst_id)