diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 58ebf7f2..cadbcc6f 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.123.0"
+ ".": "0.124.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index 47b8d19d..277eb3b9 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 190
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/lithic/lithic-0f374e78a0212145a2f55a55dfc39a612de19094d5152ae26b1bc74b01b9e343.yml
-openapi_spec_hash: ec888cdaebea979a2cd6231ca04c346c
-config_hash: 01dfc901bb6d54b0f582155d779bcbe0
+configured_endpoints: 192
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/lithic/lithic-00f07b0edcc0c3c5ef79920ced7f58dac2434df5e4c27ff6041783e8228315f9.yml
+openapi_spec_hash: 963688b09480159a06865075c94a2577
+config_hash: 265a2b679964f4ad5706de101ad2a942
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 376aab01..e3f92b71 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# Changelog
+## 0.124.0 (2026-05-08)
+
+Full Changelog: [v0.123.0...v0.124.0](https://github.com/lithic-com/lithic-python/compare/v0.123.0...v0.124.0)
+
+### Features
+
+* **api:** add retrieve_signals method to accounts and cards ([6e1649a](https://github.com/lithic-com/lithic-python/commit/6e1649ad7ee7ca58316891747b24c3f4773553d9))
+* **api:** add unit parameter and travel/distance attributes to auth_rules ([02bd0f0](https://github.com/lithic-com/lithic-python/commit/02bd0f021353f6ddb9e930f3e5515c0636adfb29))
+
+
+### Bug Fixes
+
+* **client:** add missing f-string prefix in file type error message ([826938a](https://github.com/lithic-com/lithic-python/commit/826938aec1590a333b0e377dc6f99c9e14e96cdf))
+
## 0.123.0 (2026-05-06)
Full Changelog: [v0.122.0...v0.123.0](https://github.com/lithic-com/lithic-python/compare/v0.122.0...v0.123.0)
diff --git a/api.md b/api.md
index 460379dd..2a91f069 100644
--- a/api.md
+++ b/api.md
@@ -39,6 +39,7 @@ Methods:
- client.accounts.retrieve(account_token) -> Account
- client.accounts.update(account_token, \*\*params) -> Account
- client.accounts.list(\*\*params) -> SyncCursorPage[Account]
+- client.accounts.retrieve_signals(account_token) -> SignalsResponse
- client.accounts.retrieve_spend_limits(account_token) -> AccountSpendLimits
# AccountHolders
@@ -88,6 +89,12 @@ Methods:
# AuthRules
+Types:
+
+```python
+from lithic.types import SignalsResponse
+```
+
## V2
Types:
@@ -233,6 +240,7 @@ Methods:
- client.cards.provision(card_token, \*\*params) -> CardProvisionResponse
- client.cards.reissue(card_token, \*\*params) -> Card
- client.cards.renew(card_token, \*\*params) -> Card
+- client.cards.retrieve_signals(card_token) -> SignalsResponse
- client.cards.retrieve_spend_limits(card_token) -> CardSpendLimits
- client.cards.search_by_pan(\*\*params) -> Card
- client.cards.web_provision(card_token, \*\*params) -> CardWebProvisionResponse
diff --git a/pyproject.toml b/pyproject.toml
index b9fd106b..5ad1d5e3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "lithic"
-version = "0.123.0"
+version = "0.124.0"
description = "The official Python library for the lithic API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/lithic/_files.py b/src/lithic/_files.py
index 0fdce17b..76da9e08 100644
--- a/src/lithic/_files.py
+++ b/src/lithic/_files.py
@@ -99,7 +99,7 @@ async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles
elif is_sequence_t(files):
files = [(key, await _async_transform_file(file)) for key, file in files]
else:
- raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence")
+ raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence")
return files
diff --git a/src/lithic/_version.py b/src/lithic/_version.py
index 2236ca43..af0c2a6d 100644
--- a/src/lithic/_version.py
+++ b/src/lithic/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "lithic"
-__version__ = "0.123.0" # x-release-please-version
+__version__ = "0.124.0" # x-release-please-version
diff --git a/src/lithic/resources/accounts.py b/src/lithic/resources/accounts.py
index 073f1320..b7c3b401 100644
--- a/src/lithic/resources/accounts.py
+++ b/src/lithic/resources/accounts.py
@@ -18,6 +18,7 @@
from ..pagination import SyncCursorPage, AsyncCursorPage
from .._base_client import AsyncPaginator, make_request_options
from ..types.account import Account
+from ..types.signals_response import SignalsResponse
from ..types.account_spend_limits import AccountSpendLimits
__all__ = ["Accounts", "AsyncAccounts"]
@@ -259,6 +260,46 @@ def list(
model=Account,
)
+ def retrieve_signals(
+ self,
+ account_token: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SignalsResponse:
+ """
+ Returns behavioral feature state derived from an account's transaction history.
+
+ These signals expose the same data used by behavioral rule attributes (e.g.
+ `AMOUNT_Z_SCORE` with `scope: ACCOUNT`, `IS_NEW_COUNTRY` with `scope: ACCOUNT`)
+ and custom code `TRANSACTION_HISTORY_SIGNALS` features, allowing clients to
+ inspect feature values before writing rules and debug rule behavior.
+
+ Note: 3DS fields are not available at the account scope and will be null.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not account_token:
+ raise ValueError(f"Expected a non-empty value for `account_token` but received {account_token!r}")
+ return self._get(
+ path_template("/v1/accounts/{account_token}/signals", account_token=account_token),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=SignalsResponse,
+ )
+
def retrieve_spend_limits(
self,
account_token: str,
@@ -533,6 +574,46 @@ def list(
model=Account,
)
+ async def retrieve_signals(
+ self,
+ account_token: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SignalsResponse:
+ """
+ Returns behavioral feature state derived from an account's transaction history.
+
+ These signals expose the same data used by behavioral rule attributes (e.g.
+ `AMOUNT_Z_SCORE` with `scope: ACCOUNT`, `IS_NEW_COUNTRY` with `scope: ACCOUNT`)
+ and custom code `TRANSACTION_HISTORY_SIGNALS` features, allowing clients to
+ inspect feature values before writing rules and debug rule behavior.
+
+ Note: 3DS fields are not available at the account scope and will be null.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not account_token:
+ raise ValueError(f"Expected a non-empty value for `account_token` but received {account_token!r}")
+ return await self._get(
+ path_template("/v1/accounts/{account_token}/signals", account_token=account_token),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=SignalsResponse,
+ )
+
async def retrieve_spend_limits(
self,
account_token: str,
@@ -584,6 +665,9 @@ def __init__(self, accounts: Accounts) -> None:
self.list = _legacy_response.to_raw_response_wrapper(
accounts.list,
)
+ self.retrieve_signals = _legacy_response.to_raw_response_wrapper(
+ accounts.retrieve_signals,
+ )
self.retrieve_spend_limits = _legacy_response.to_raw_response_wrapper(
accounts.retrieve_spend_limits,
)
@@ -602,6 +686,9 @@ def __init__(self, accounts: AsyncAccounts) -> None:
self.list = _legacy_response.async_to_raw_response_wrapper(
accounts.list,
)
+ self.retrieve_signals = _legacy_response.async_to_raw_response_wrapper(
+ accounts.retrieve_signals,
+ )
self.retrieve_spend_limits = _legacy_response.async_to_raw_response_wrapper(
accounts.retrieve_spend_limits,
)
@@ -620,6 +707,9 @@ def __init__(self, accounts: Accounts) -> None:
self.list = to_streamed_response_wrapper(
accounts.list,
)
+ self.retrieve_signals = to_streamed_response_wrapper(
+ accounts.retrieve_signals,
+ )
self.retrieve_spend_limits = to_streamed_response_wrapper(
accounts.retrieve_spend_limits,
)
@@ -638,6 +728,9 @@ def __init__(self, accounts: AsyncAccounts) -> None:
self.list = async_to_streamed_response_wrapper(
accounts.list,
)
+ self.retrieve_signals = async_to_streamed_response_wrapper(
+ accounts.retrieve_signals,
+ )
self.retrieve_spend_limits = async_to_streamed_response_wrapper(
accounts.retrieve_spend_limits,
)
diff --git a/src/lithic/resources/cards/cards.py b/src/lithic/resources/cards/cards.py
index 3bd8eccd..0ab13db6 100644
--- a/src/lithic/resources/cards/cards.py
+++ b/src/lithic/resources/cards/cards.py
@@ -53,6 +53,7 @@
FinancialTransactionsWithStreamingResponse,
AsyncFinancialTransactionsWithStreamingResponse,
)
+from ...types.signals_response import SignalsResponse
from ...types.card_spend_limits import CardSpendLimits
from ...types.spend_limit_duration import SpendLimitDuration
from ...types.shared_params.carrier import Carrier
@@ -1110,6 +1111,44 @@ def renew(
cast_to=Card,
)
+ def retrieve_signals(
+ self,
+ card_token: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SignalsResponse:
+ """
+ Returns behavioral feature state derived from a card's transaction history.
+
+ These signals expose the same data used by behavioral rule attributes (e.g.
+ `AMOUNT_Z_SCORE` with `scope: CARD`, `IS_NEW_COUNTRY` with `scope: CARD`) and
+ custom code `TRANSACTION_HISTORY_SIGNALS` features, allowing clients to inspect
+ feature values before writing rules and debug rule behavior.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not card_token:
+ raise ValueError(f"Expected a non-empty value for `card_token` but received {card_token!r}")
+ return self._get(
+ path_template("/v1/cards/{card_token}/signals", card_token=card_token),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=SignalsResponse,
+ )
+
def retrieve_spend_limits(
self,
card_token: str,
@@ -2300,6 +2339,44 @@ async def renew(
cast_to=Card,
)
+ async def retrieve_signals(
+ self,
+ card_token: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SignalsResponse:
+ """
+ Returns behavioral feature state derived from a card's transaction history.
+
+ These signals expose the same data used by behavioral rule attributes (e.g.
+ `AMOUNT_Z_SCORE` with `scope: CARD`, `IS_NEW_COUNTRY` with `scope: CARD`) and
+ custom code `TRANSACTION_HISTORY_SIGNALS` features, allowing clients to inspect
+ feature values before writing rules and debug rule behavior.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not card_token:
+ raise ValueError(f"Expected a non-empty value for `card_token` but received {card_token!r}")
+ return await self._get(
+ path_template("/v1/cards/{card_token}/signals", card_token=card_token),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=SignalsResponse,
+ )
+
async def retrieve_spend_limits(
self,
card_token: str,
@@ -2474,6 +2551,9 @@ def __init__(self, cards: Cards) -> None:
self.renew = _legacy_response.to_raw_response_wrapper(
cards.renew,
)
+ self.retrieve_signals = _legacy_response.to_raw_response_wrapper(
+ cards.retrieve_signals,
+ )
self.retrieve_spend_limits = _legacy_response.to_raw_response_wrapper(
cards.retrieve_spend_limits,
)
@@ -2524,6 +2604,9 @@ def __init__(self, cards: AsyncCards) -> None:
self.renew = _legacy_response.async_to_raw_response_wrapper(
cards.renew,
)
+ self.retrieve_signals = _legacy_response.async_to_raw_response_wrapper(
+ cards.retrieve_signals,
+ )
self.retrieve_spend_limits = _legacy_response.async_to_raw_response_wrapper(
cards.retrieve_spend_limits,
)
@@ -2574,6 +2657,9 @@ def __init__(self, cards: Cards) -> None:
self.renew = to_streamed_response_wrapper(
cards.renew,
)
+ self.retrieve_signals = to_streamed_response_wrapper(
+ cards.retrieve_signals,
+ )
self.retrieve_spend_limits = to_streamed_response_wrapper(
cards.retrieve_spend_limits,
)
@@ -2624,6 +2710,9 @@ def __init__(self, cards: AsyncCards) -> None:
self.renew = async_to_streamed_response_wrapper(
cards.renew,
)
+ self.retrieve_signals = async_to_streamed_response_wrapper(
+ cards.retrieve_signals,
+ )
self.retrieve_spend_limits = async_to_streamed_response_wrapper(
cards.retrieve_spend_limits,
)
diff --git a/src/lithic/types/__init__.py b/src/lithic/types/__init__.py
index 0f829753..33f2749e 100644
--- a/src/lithic/types/__init__.py
+++ b/src/lithic/types/__init__.py
@@ -47,6 +47,7 @@
from .hold_list_params import HoldListParams as HoldListParams
from .hold_void_params import HoldVoidParams as HoldVoidParams
from .kyc_exempt_param import KYCExemptParam as KYCExemptParam
+from .signals_response import SignalsResponse as SignalsResponse
from .statement_totals import StatementTotals as StatementTotals
from .card_embed_params import CardEmbedParams as CardEmbedParams
from .card_renew_params import CardRenewParams as CardRenewParams
diff --git a/src/lithic/types/auth_rules/conditional_authorization_action_parameters.py b/src/lithic/types/auth_rules/conditional_authorization_action_parameters.py
index 3c747d40..4b288ac2 100644
--- a/src/lithic/types/auth_rules/conditional_authorization_action_parameters.py
+++ b/src/lithic/types/auth_rules/conditional_authorization_action_parameters.py
@@ -11,12 +11,13 @@
class ConditionParameters(BaseModel):
- """Additional parameters required for transaction history signal attributes.
+ """Additional parameters for certain attributes.
- Required when
- `attribute` is one of `AMOUNT_Z_SCORE`, `AVG_TRANSACTION_AMOUNT`,
- `STDEV_TRANSACTION_AMOUNT`, `IS_NEW_COUNTRY`, `IS_NEW_MCC`, `IS_FIRST_TRANSACTION`,
- `CONSECUTIVE_DECLINES`, `TIME_SINCE_LAST_TRANSACTION`, or `DISTINCT_COUNTRY_COUNT`.
+ Required when `attribute` is one of
+ `AMOUNT_Z_SCORE`, `AVG_TRANSACTION_AMOUNT`, `STDEV_TRANSACTION_AMOUNT`,
+ `IS_NEW_COUNTRY`, `IS_NEW_MCC`, `IS_FIRST_TRANSACTION`, `CONSECUTIVE_DECLINES`,
+ `TIME_SINCE_LAST_TRANSACTION`, or `DISTINCT_COUNTRY_COUNT` (require `scope`);
+ or `TRAVEL_SPEED` or `DISTANCE_FROM_LAST_TRANSACTION` (require `unit`).
Not used for other attributes.
"""
@@ -30,6 +31,16 @@ class ConditionParameters(BaseModel):
scope: Optional[Literal["CARD", "ACCOUNT", "BUSINESS_ACCOUNT"]] = None
"""The entity scope to evaluate the attribute against."""
+ unit: Optional[Literal["MPH", "KPH", "MILES", "KILOMETERS"]] = None
+ """The unit for impossible travel attributes.
+
+ Required when `attribute` is `TRAVEL_SPEED` or `DISTANCE_FROM_LAST_TRANSACTION`.
+
+ For `TRAVEL_SPEED`: `MPH` (miles per hour) or `KPH` (kilometers per hour).
+
+ For `DISTANCE_FROM_LAST_TRANSACTION`: `MILES` or `KILOMETERS`.
+ """
+
class Condition(BaseModel):
attribute: Literal[
@@ -70,6 +81,8 @@ class Condition(BaseModel):
"DISTINCT_COUNTRY_COUNT",
"IS_NEW_MERCHANT",
"THREE_DS_SUCCESS_RATE",
+ "TRAVEL_SPEED",
+ "DISTANCE_FROM_LAST_TRANSACTION",
]
"""The attribute to target.
@@ -173,6 +186,15 @@ class Condition(BaseModel):
`parameters` required.
- `THREE_DS_SUCCESS_RATE`: The 3DS authentication success rate for the card, as
a percentage from 0.0 to 100.0. Card-scoped only; no `parameters` required.
+ - `TRAVEL_SPEED`: The estimated speed of travel derived from the distance
+ between the postal code centers of the last card-present transaction and the
+ current transaction, divided by the elapsed time. Null if there is no prior
+ card-present transaction, if either postal code cannot be geocoded, or if
+ elapsed time is zero. Requires `parameters.unit` set to `MPH` or `KPH`.
+ - `DISTANCE_FROM_LAST_TRANSACTION`: The estimated distance between the postal
+ code centers of the last card-present transaction and the current transaction.
+ Null if there is no prior card-present transaction or if either postal code
+ cannot be geocoded. Requires `parameters.unit` set to `MILES` or `KILOMETERS`.
"""
operation: ConditionalOperation
@@ -182,12 +204,14 @@ class Condition(BaseModel):
"""A regex string, to be used with `MATCHES` or `DOES_NOT_MATCH`"""
parameters: Optional[ConditionParameters] = None
- """Additional parameters required for transaction history signal attributes.
+ """Additional parameters for certain attributes.
Required when `attribute` is one of `AMOUNT_Z_SCORE`, `AVG_TRANSACTION_AMOUNT`,
`STDEV_TRANSACTION_AMOUNT`, `IS_NEW_COUNTRY`, `IS_NEW_MCC`,
`IS_FIRST_TRANSACTION`, `CONSECUTIVE_DECLINES`, `TIME_SINCE_LAST_TRANSACTION`,
- or `DISTINCT_COUNTRY_COUNT`. Not used for other attributes.
+ or `DISTINCT_COUNTRY_COUNT` (require `scope`); or `TRAVEL_SPEED` or
+ `DISTANCE_FROM_LAST_TRANSACTION` (require `unit`). Not used for other
+ attributes.
"""
diff --git a/src/lithic/types/auth_rules/conditional_authorization_action_parameters_param.py b/src/lithic/types/auth_rules/conditional_authorization_action_parameters_param.py
index a70a27bd..e903a5e0 100644
--- a/src/lithic/types/auth_rules/conditional_authorization_action_parameters_param.py
+++ b/src/lithic/types/auth_rules/conditional_authorization_action_parameters_param.py
@@ -13,12 +13,13 @@
class ConditionParameters(TypedDict, total=False):
- """Additional parameters required for transaction history signal attributes.
+ """Additional parameters for certain attributes.
- Required when
- `attribute` is one of `AMOUNT_Z_SCORE`, `AVG_TRANSACTION_AMOUNT`,
- `STDEV_TRANSACTION_AMOUNT`, `IS_NEW_COUNTRY`, `IS_NEW_MCC`, `IS_FIRST_TRANSACTION`,
- `CONSECUTIVE_DECLINES`, `TIME_SINCE_LAST_TRANSACTION`, or `DISTINCT_COUNTRY_COUNT`.
+ Required when `attribute` is one of
+ `AMOUNT_Z_SCORE`, `AVG_TRANSACTION_AMOUNT`, `STDEV_TRANSACTION_AMOUNT`,
+ `IS_NEW_COUNTRY`, `IS_NEW_MCC`, `IS_FIRST_TRANSACTION`, `CONSECUTIVE_DECLINES`,
+ `TIME_SINCE_LAST_TRANSACTION`, or `DISTINCT_COUNTRY_COUNT` (require `scope`);
+ or `TRAVEL_SPEED` or `DISTANCE_FROM_LAST_TRANSACTION` (require `unit`).
Not used for other attributes.
"""
@@ -32,6 +33,16 @@ class ConditionParameters(TypedDict, total=False):
scope: Literal["CARD", "ACCOUNT", "BUSINESS_ACCOUNT"]
"""The entity scope to evaluate the attribute against."""
+ unit: Literal["MPH", "KPH", "MILES", "KILOMETERS"]
+ """The unit for impossible travel attributes.
+
+ Required when `attribute` is `TRAVEL_SPEED` or `DISTANCE_FROM_LAST_TRANSACTION`.
+
+ For `TRAVEL_SPEED`: `MPH` (miles per hour) or `KPH` (kilometers per hour).
+
+ For `DISTANCE_FROM_LAST_TRANSACTION`: `MILES` or `KILOMETERS`.
+ """
+
class Condition(TypedDict, total=False):
attribute: Required[
@@ -73,6 +84,8 @@ class Condition(TypedDict, total=False):
"DISTINCT_COUNTRY_COUNT",
"IS_NEW_MERCHANT",
"THREE_DS_SUCCESS_RATE",
+ "TRAVEL_SPEED",
+ "DISTANCE_FROM_LAST_TRANSACTION",
]
]
"""The attribute to target.
@@ -177,6 +190,15 @@ class Condition(TypedDict, total=False):
`parameters` required.
- `THREE_DS_SUCCESS_RATE`: The 3DS authentication success rate for the card, as
a percentage from 0.0 to 100.0. Card-scoped only; no `parameters` required.
+ - `TRAVEL_SPEED`: The estimated speed of travel derived from the distance
+ between the postal code centers of the last card-present transaction and the
+ current transaction, divided by the elapsed time. Null if there is no prior
+ card-present transaction, if either postal code cannot be geocoded, or if
+ elapsed time is zero. Requires `parameters.unit` set to `MPH` or `KPH`.
+ - `DISTANCE_FROM_LAST_TRANSACTION`: The estimated distance between the postal
+ code centers of the last card-present transaction and the current transaction.
+ Null if there is no prior card-present transaction or if either postal code
+ cannot be geocoded. Requires `parameters.unit` set to `MILES` or `KILOMETERS`.
"""
operation: Required[ConditionalOperation]
@@ -186,12 +208,14 @@ class Condition(TypedDict, total=False):
"""A regex string, to be used with `MATCHES` or `DOES_NOT_MATCH`"""
parameters: ConditionParameters
- """Additional parameters required for transaction history signal attributes.
+ """Additional parameters for certain attributes.
Required when `attribute` is one of `AMOUNT_Z_SCORE`, `AVG_TRANSACTION_AMOUNT`,
`STDEV_TRANSACTION_AMOUNT`, `IS_NEW_COUNTRY`, `IS_NEW_MCC`,
`IS_FIRST_TRANSACTION`, `CONSECUTIVE_DECLINES`, `TIME_SINCE_LAST_TRANSACTION`,
- or `DISTINCT_COUNTRY_COUNT`. Not used for other attributes.
+ or `DISTINCT_COUNTRY_COUNT` (require `scope`); or `TRAVEL_SPEED` or
+ `DISTANCE_FROM_LAST_TRANSACTION` (require `unit`). Not used for other
+ attributes.
"""
diff --git a/src/lithic/types/signals_response.py b/src/lithic/types/signals_response.py
new file mode 100644
index 00000000..5fc46d6d
--- /dev/null
+++ b/src/lithic/types/signals_response.py
@@ -0,0 +1,186 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+from datetime import datetime
+
+from .._models import BaseModel
+
+__all__ = ["SignalsResponse"]
+
+
+class SignalsResponse(BaseModel):
+ """
+ Behavioral feature state for a card or account derived from its transaction history.
+
+ Derived statistical features (averages, standard deviations, z-scores) are computed using Welford's online algorithm over approved transactions. Average fields are null when fewer than 5 approved transactions have been recorded. Standard deviation fields are null when fewer than 30 approved transactions have been recorded.
+
+ 3DS fields (`three_ds_success_rate`, `three_ds_success_count`, `three_ds_total_count`) are card-scoped and will be null for account responses.
+
+ Raw fields (`seen_countries`, `seen_mccs`, `approved_txn_amount_m2`, etc.) are included so clients can compute their own transaction-specific derivations, such as checking whether a new transaction's country is in `seen_countries` to determine `is_new_country`, or computing a z-score using the raw mean and M2 values.
+ """
+
+ approved_txn_amount_m2: Optional[float] = None
+ """The Welford M2 accumulator for lifetime approved transaction amounts.
+
+ Used together with `avg_transaction_amount` and `approved_txn_count` to compute
+ the z-score of a new transaction amount (variance = M2 / (count - 1)).
+ """
+
+ approved_txn_amount_m2_30d: Optional[float] = None
+ """
+ The Welford M2 accumulator for approved transaction amounts over the last 30
+ days.
+ """
+
+ approved_txn_amount_m2_7d: Optional[float] = None
+ """
+ The Welford M2 accumulator for approved transaction amounts over the last 7
+ days.
+ """
+
+ approved_txn_amount_m2_90d: Optional[float] = None
+ """
+ The Welford M2 accumulator for approved transaction amounts over the last 90
+ days.
+ """
+
+ approved_txn_count: Optional[int] = None
+ """The total number of approved transactions over the entity's lifetime."""
+
+ approved_txn_count_30d: Optional[int] = None
+ """The number of approved transactions in the last 30 days."""
+
+ approved_txn_count_7d: Optional[int] = None
+ """The number of approved transactions in the last 7 days."""
+
+ approved_txn_count_90d: Optional[int] = None
+ """The number of approved transactions in the last 90 days."""
+
+ avg_transaction_amount: Optional[float] = None
+ """The average approved transaction amount over the entity's lifetime, in cents.
+
+ Null if fewer than 5 approved transactions have been recorded.
+ """
+
+ avg_transaction_amount_30d: Optional[float] = None
+ """The average approved transaction amount over the last 30 days, in cents.
+
+ Null if fewer than 5 approved transactions in window.
+ """
+
+ avg_transaction_amount_7d: Optional[float] = None
+ """The average approved transaction amount over the last 7 days, in cents.
+
+ Null if fewer than 5 approved transactions in window.
+ """
+
+ avg_transaction_amount_90d: Optional[float] = None
+ """The average approved transaction amount over the last 90 days, in cents.
+
+ Null if fewer than 5 approved transactions in window.
+ """
+
+ distinct_country_count: Optional[int] = None
+ """
+ The number of distinct merchant countries seen in the entity's transaction
+ history.
+ """
+
+ distinct_mcc_count: Optional[int] = None
+ """The number of distinct MCCs seen in the entity's transaction history."""
+
+ first_txn_at: Optional[datetime] = None
+ """
+ The timestamp of the first approved transaction for the entity, in ISO 8601
+ format.
+ """
+
+ is_first_transaction: Optional[bool] = None
+ """Whether the entity has no prior transaction history.
+
+ Returns true if no history is found. Null if transaction history exists but a
+ first transaction timestamp is unavailable.
+ """
+
+ last_cp_country: Optional[str] = None
+ """The merchant country of the last card-present transaction."""
+
+ last_cp_postal_code: Optional[str] = None
+ """The merchant postal code of the last card-present transaction."""
+
+ last_cp_timestamp: Optional[datetime] = None
+ """The timestamp of the last card-present transaction, in ISO 8601 format."""
+
+ last_txn_approved_at: Optional[datetime] = None
+ """
+ The timestamp of the most recent approved transaction for the entity, in ISO
+ 8601 format.
+ """
+
+ seen_countries: Optional[List[str]] = None
+ """The set of merchant countries seen in the entity's transaction history.
+
+ Clients can use this to determine whether a new transaction's country is novel
+ (i.e. compute `is_new_country`).
+ """
+
+ seen_mccs: Optional[List[str]] = None
+ """The set of MCCs seen in the entity's transaction history.
+
+ Clients can use this to determine whether a new transaction's MCC is novel (i.e.
+ compute `is_new_mcc`).
+ """
+
+ seen_merchants: Optional[List[str]] = None
+ """
+ The set of card acceptor IDs seen in the card's approved transaction history,
+ capped at the 1000 most recently seen. Null for account responses. Clients can
+ use this to determine whether a new transaction's merchant is novel (i.e.
+ compute `is_new_merchant`).
+ """
+
+ stdev_transaction_amount: Optional[float] = None
+ """
+ The standard deviation of approved transaction amounts over the entity's
+ lifetime, in cents. Null if fewer than 30 approved transactions have been
+ recorded.
+ """
+
+ stdev_transaction_amount_30d: Optional[float] = None
+ """
+ The standard deviation of approved transaction amounts over the last 30 days, in
+ cents. Null if fewer than 30 approved transactions in window.
+ """
+
+ stdev_transaction_amount_7d: Optional[float] = None
+ """
+ The standard deviation of approved transaction amounts over the last 7 days, in
+ cents. Null if fewer than 30 approved transactions in window.
+ """
+
+ stdev_transaction_amount_90d: Optional[float] = None
+ """
+ The standard deviation of approved transaction amounts over the last 90 days, in
+ cents. Null if fewer than 30 approved transactions in window.
+ """
+
+ three_ds_success_count: Optional[int] = None
+ """The number of successful 3DS authentications for the card.
+
+ Null for account responses.
+ """
+
+ three_ds_success_rate: Optional[float] = None
+ """
+ The 3DS authentication success rate for the card, as a percentage from 0.0 to
+ 100.0. Null for account responses.
+ """
+
+ three_ds_total_count: Optional[int] = None
+ """The total number of 3DS authentication attempts for the card.
+
+ Null for account responses.
+ """
+
+ time_since_last_transaction_days: Optional[float] = None
+ """The number of days since the last approved transaction on the entity."""
diff --git a/tests/api_resources/test_accounts.py b/tests/api_resources/test_accounts.py
index e9938613..2111305b 100644
--- a/tests/api_resources/test_accounts.py
+++ b/tests/api_resources/test_accounts.py
@@ -9,7 +9,7 @@
from lithic import Lithic, AsyncLithic
from tests.utils import assert_matches_type
-from lithic.types import Account, AccountSpendLimits
+from lithic.types import Account, SignalsResponse, AccountSpendLimits
from lithic._utils import parse_datetime
from lithic.pagination import SyncCursorPage, AsyncCursorPage
@@ -157,6 +157,44 @@ def test_streaming_response_list(self, client: Lithic) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ def test_method_retrieve_signals(self, client: Lithic) -> None:
+ account = client.accounts.retrieve_signals(
+ "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
+ )
+ assert_matches_type(SignalsResponse, account, path=["response"])
+
+ @parametrize
+ def test_raw_response_retrieve_signals(self, client: Lithic) -> None:
+ response = client.accounts.with_raw_response.retrieve_signals(
+ "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ account = response.parse()
+ assert_matches_type(SignalsResponse, account, path=["response"])
+
+ @parametrize
+ def test_streaming_response_retrieve_signals(self, client: Lithic) -> None:
+ with client.accounts.with_streaming_response.retrieve_signals(
+ "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ account = response.parse()
+ assert_matches_type(SignalsResponse, account, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_retrieve_signals(self, client: Lithic) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `account_token` but received ''"):
+ client.accounts.with_raw_response.retrieve_signals(
+ "",
+ )
+
@parametrize
def test_method_retrieve_spend_limits(self, client: Lithic) -> None:
account = client.accounts.retrieve_spend_limits(
@@ -339,6 +377,44 @@ async def test_streaming_response_list(self, async_client: AsyncLithic) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ async def test_method_retrieve_signals(self, async_client: AsyncLithic) -> None:
+ account = await async_client.accounts.retrieve_signals(
+ "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
+ )
+ assert_matches_type(SignalsResponse, account, path=["response"])
+
+ @parametrize
+ async def test_raw_response_retrieve_signals(self, async_client: AsyncLithic) -> None:
+ response = await async_client.accounts.with_raw_response.retrieve_signals(
+ "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ account = response.parse()
+ assert_matches_type(SignalsResponse, account, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_retrieve_signals(self, async_client: AsyncLithic) -> None:
+ async with async_client.accounts.with_streaming_response.retrieve_signals(
+ "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ account = await response.parse()
+ assert_matches_type(SignalsResponse, account, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_retrieve_signals(self, async_client: AsyncLithic) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `account_token` but received ''"):
+ await async_client.accounts.with_raw_response.retrieve_signals(
+ "",
+ )
+
@parametrize
async def test_method_retrieve_spend_limits(self, async_client: AsyncLithic) -> None:
account = await async_client.accounts.retrieve_spend_limits(
diff --git a/tests/api_resources/test_cards.py b/tests/api_resources/test_cards.py
index e7f30b11..1e289d72 100644
--- a/tests/api_resources/test_cards.py
+++ b/tests/api_resources/test_cards.py
@@ -13,6 +13,7 @@
Card,
NonPCICard,
CardSpendLimits,
+ SignalsResponse,
CardProvisionResponse,
CardWebProvisionResponse,
)
@@ -580,6 +581,44 @@ def test_path_params_renew(self, client: Lithic) -> None:
},
)
+ @parametrize
+ def test_method_retrieve_signals(self, client: Lithic) -> None:
+ card = client.cards.retrieve_signals(
+ "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
+ )
+ assert_matches_type(SignalsResponse, card, path=["response"])
+
+ @parametrize
+ def test_raw_response_retrieve_signals(self, client: Lithic) -> None:
+ response = client.cards.with_raw_response.retrieve_signals(
+ "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ card = response.parse()
+ assert_matches_type(SignalsResponse, card, path=["response"])
+
+ @parametrize
+ def test_streaming_response_retrieve_signals(self, client: Lithic) -> None:
+ with client.cards.with_streaming_response.retrieve_signals(
+ "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ card = response.parse()
+ assert_matches_type(SignalsResponse, card, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_retrieve_signals(self, client: Lithic) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `card_token` but received ''"):
+ client.cards.with_raw_response.retrieve_signals(
+ "",
+ )
+
@parametrize
def test_method_retrieve_spend_limits(self, client: Lithic) -> None:
card = client.cards.retrieve_spend_limits(
@@ -1259,6 +1298,44 @@ async def test_path_params_renew(self, async_client: AsyncLithic) -> None:
},
)
+ @parametrize
+ async def test_method_retrieve_signals(self, async_client: AsyncLithic) -> None:
+ card = await async_client.cards.retrieve_signals(
+ "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
+ )
+ assert_matches_type(SignalsResponse, card, path=["response"])
+
+ @parametrize
+ async def test_raw_response_retrieve_signals(self, async_client: AsyncLithic) -> None:
+ response = await async_client.cards.with_raw_response.retrieve_signals(
+ "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ card = response.parse()
+ assert_matches_type(SignalsResponse, card, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_retrieve_signals(self, async_client: AsyncLithic) -> None:
+ async with async_client.cards.with_streaming_response.retrieve_signals(
+ "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ card = await response.parse()
+ assert_matches_type(SignalsResponse, card, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_retrieve_signals(self, async_client: AsyncLithic) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `card_token` but received ''"):
+ await async_client.cards.with_raw_response.retrieve_signals(
+ "",
+ )
+
@parametrize
async def test_method_retrieve_spend_limits(self, async_client: AsyncLithic) -> None:
card = await async_client.cards.retrieve_spend_limits(