From 209d9b852cee0478d836e0679dd7ed2f7d81d6a2 Mon Sep 17 00:00:00 2001
From: "fern-api[bot]" <115122769+fern-api[bot]@users.noreply.github.com>
Date: Tue, 28 Jul 2026 18:32:03 +0000
Subject: [PATCH 1/3] [fern-generated] Update SDK
Generated by Fern
CLI Version: unknown
Generators:
- fernapi/fern-python-sdk: 5.12.12
---
.fern/metadata.json | 6 +-
reference.md | 415 ++++++++++
src/smallestai/atoms/__init__.py | 53 ++
src/smallestai/atoms/billing/__init__.py | 37 +
src/smallestai/atoms/billing/client.py | 518 ++++++++++++
src/smallestai/atoms/billing/raw_client.py | 747 ++++++++++++++++++
.../atoms/billing/types/__init__.py | 38 +
.../types/get_ledger_billing_request_scope.py | 5 +
.../types/get_ledger_billing_request_type.py | 17 +
...get_calls_response_data_transcript_item.py | 4 +-
src/smallestai/atoms/client.py | 19 +
src/smallestai/atoms/types/__init__.py | 45 ++
.../atoms/types/bad_request_error_body.py | 5 +-
.../types/bad_request_error_body_error.py | 21 +
.../atoms/types/billing_balance_response.py | 22 +
.../types/billing_balance_response_data.py | 41 +
.../types/billing_invoice_list_response.py | 24 +
.../types/billing_invoice_pdf_response.py | 22 +
.../billing_invoice_pdf_response_data.py | 29 +
.../atoms/types/billing_ledger_entry.py | 73 ++
.../billing_ledger_entry_transaction_type.py | 16 +
.../atoms/types/billing_ledger_response.py | 22 +
.../types/billing_ledger_response_data.py | 37 +
.../billing_ledger_response_data_period.py | 34 +
.../billing_ledger_response_data_summary.py | 36 +
.../types/billing_usage_breakdown_response.py | 22 +
.../billing_usage_breakdown_response_data.py | 41 +
.../atoms/types/not_found_error_body.py | 5 +-
.../atoms/types/not_found_error_body_error.py | 21 +
src/smallestai/environment.py | 8 +-
tests/wire/conftest.py | 4 +-
tests/wire/test_atoms_billing.py | 50 ++
wiremock/wiremock-mappings.json | 174 +++-
33 files changed, 2597 insertions(+), 14 deletions(-)
create mode 100644 src/smallestai/atoms/billing/__init__.py
create mode 100644 src/smallestai/atoms/billing/client.py
create mode 100644 src/smallestai/atoms/billing/raw_client.py
create mode 100644 src/smallestai/atoms/billing/types/__init__.py
create mode 100644 src/smallestai/atoms/billing/types/get_ledger_billing_request_scope.py
create mode 100644 src/smallestai/atoms/billing/types/get_ledger_billing_request_type.py
create mode 100644 src/smallestai/atoms/types/bad_request_error_body_error.py
create mode 100644 src/smallestai/atoms/types/billing_balance_response.py
create mode 100644 src/smallestai/atoms/types/billing_balance_response_data.py
create mode 100644 src/smallestai/atoms/types/billing_invoice_list_response.py
create mode 100644 src/smallestai/atoms/types/billing_invoice_pdf_response.py
create mode 100644 src/smallestai/atoms/types/billing_invoice_pdf_response_data.py
create mode 100644 src/smallestai/atoms/types/billing_ledger_entry.py
create mode 100644 src/smallestai/atoms/types/billing_ledger_entry_transaction_type.py
create mode 100644 src/smallestai/atoms/types/billing_ledger_response.py
create mode 100644 src/smallestai/atoms/types/billing_ledger_response_data.py
create mode 100644 src/smallestai/atoms/types/billing_ledger_response_data_period.py
create mode 100644 src/smallestai/atoms/types/billing_ledger_response_data_summary.py
create mode 100644 src/smallestai/atoms/types/billing_usage_breakdown_response.py
create mode 100644 src/smallestai/atoms/types/billing_usage_breakdown_response_data.py
create mode 100644 src/smallestai/atoms/types/not_found_error_body_error.py
create mode 100644 tests/wire/test_atoms_billing.py
diff --git a/.fern/metadata.json b/.fern/metadata.json
index 105761d3..bbf2558d 100644
--- a/.fern/metadata.json
+++ b/.fern/metadata.json
@@ -45,10 +45,10 @@
}
]
},
- "originGitCommit": "c3e8467d7de3ac4b4d54e3c9bc4828bebfc4c792",
+ "originGitCommit": "e9ca686b515042154390e137de05194b4a191566",
"originGitCommitIsDirty": false,
"invokedBy": "ci",
- "requestedVersion": "5.3.0",
+ "requestedVersion": "5.3.1",
"ciProvider": "github",
- "sdkVersion": "5.3.0"
+ "sdkVersion": "5.3.1"
}
\ No newline at end of file
diff --git a/reference.md b/reference.md
index 2e8e9097..9b91ef71 100644
--- a/reference.md
+++ b/reference.md
@@ -13050,6 +13050,421 @@ the caller's org.
+
+
+
+
+## Atoms Billing
+client.atoms.billing.get_balance() -> BillingBalanceResponse
+
+-
+
+#### 📝 Description
+
+
+-
+
+
+-
+
+Returns the organization's current credit balance in USD plus the
+current plan identifier. Organization is resolved from the API key,
+so no `X-Organization-Id` header is required.
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```python
+from smallestai import SmallestAI
+from smallestai.environment import SmallestAIEnvironment
+
+client = SmallestAI(
+ api_key="",
+ environment=SmallestAIEnvironment.PRODUCTION,
+)
+
+client.atoms.billing.get_balance()
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
+
+
+
+
+client.atoms.billing.get_ledger(...) -> BillingLedgerResponse
+
+-
+
+#### 📝 Description
+
+
+-
+
+
+-
+
+Paginated credit-ledger transaction history. Ledger reads are served
+from ClickHouse; a rare storage-tier outage returns an empty
+`transactions` array rather than a 5xx.
+
+**Window rules**
+
+- `from` defaults to `to - 7 days`, `to` defaults to now.
+- The span between `from` and `to` cannot exceed **90 days**. A wider
+ span returns 400. Page through longer periods by making multiple
+ calls with shifted `from`/`to`.
+- `from` cannot be earlier than **2026-03-02T00:00:00Z**. Older
+ historical data is not available via API; contact support for bulk
+ exports.
+- `from > to` returns 400.
+
+**Filters**
+
+- `type` filters to a single transaction type. `PAYMENTS` is a
+ virtual filter that returns both `CREDIT_PURCHASE` and
+ `AUTO_RELOAD` rows.
+- `scope` filters `USAGE_DEDUCTION` rows by product category.
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```python
+from smallestai import SmallestAI
+from smallestai.environment import SmallestAIEnvironment
+import datetime
+
+client = SmallestAI(
+ api_key="",
+ environment=SmallestAIEnvironment.PRODUCTION,
+)
+
+client.atoms.billing.get_ledger(
+ from_=datetime.datetime.fromisoformat("2026-07-01T00:00:00+00:00"),
+ to=datetime.datetime.fromisoformat("2026-07-28T00:00:00+00:00"),
+)
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**limit:** `typing.Optional[int]` — Page size (1–100).
+
+
+
+
+
+-
+
+**offset:** `typing.Optional[int]` — Offset for pagination.
+
+
+
+
+
+-
+
+**from:** `typing.Optional[datetime.datetime]` — Lower bound of the query window (ISO 8601, `Z`-suffixed UTC recommended). Defaults to seven days before `to`. Cannot be earlier than `2026-03-02T00:00:00Z`.
+
+
+
+
+
+-
+
+**to:** `typing.Optional[datetime.datetime]` — Upper bound of the query window (ISO 8601, `Z`-suffixed UTC recommended). Defaults to now.
+
+
+
+
+
+-
+
+**type:** `typing.Optional[GetLedgerBillingRequestType]` — Filter to a single transaction type. `PAYMENTS` is a virtual filter that returns purchase-related rows (`CREDIT_PURCHASE` + `AUTO_RELOAD`).
+
+
+
+
+
+-
+
+**scope:** `typing.Optional[GetLedgerBillingRequestScope]` — Filter by spend category. Applies only to `USAGE_DEDUCTION` rows.
+
+
+
+
+
+-
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
+
+
+
+
+client.atoms.billing.get_usage_breakdown() -> BillingUsageBreakdownResponse
+
+-
+
+#### 📝 Description
+
+
+-
+
+
+-
+
+Total credits spent so far, split across the three product scopes.
+The window is a cumulative snapshot from **2026-03-02T00:00:00Z**
+(the platform's usage-tracking start date) up to the current
+instant. Served from ClickHouse. No query parameters.
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```python
+from smallestai import SmallestAI
+from smallestai.environment import SmallestAIEnvironment
+
+client = SmallestAI(
+ api_key="",
+ environment=SmallestAIEnvironment.PRODUCTION,
+)
+
+client.atoms.billing.get_usage_breakdown()
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
+
+
+
+
+client.atoms.billing.list_invoices() -> BillingInvoiceListResponse
+
+-
+
+#### 📝 Description
+
+
+-
+
+
+-
+
+Returns up to 20 of the most recent Stripe invoices for the caller's
+organization. Each item is the raw Stripe `Invoice` object; use the
+canonical Stripe reference at
+[stripe.com/docs/api/invoices/object](https://stripe.com/docs/api/invoices/object)
+for field-level semantics.
+
+Organizations that have never been charged (free-tier only) return
+an empty array.
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```python
+from smallestai import SmallestAI
+from smallestai.environment import SmallestAIEnvironment
+
+client = SmallestAI(
+ api_key="",
+ environment=SmallestAIEnvironment.PRODUCTION,
+)
+
+client.atoms.billing.list_invoices()
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
+
+
+
+
+client.atoms.billing.get_invoice_pdf(...) -> BillingInvoicePdfResponse
+
+-
+
+#### 📝 Description
+
+
+-
+
+
+-
+
+Returns a Stripe-hosted PDF URL for the invoice. The URL is
+short-lived; fetch fresh when you need to hand it to a user.
+
+Requesting an invoice that does not belong to the caller's
+organization returns 404 (not 403), so a foreign invoice ID cannot
+be confirmed to exist.
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```python
+from smallestai import SmallestAI
+from smallestai.environment import SmallestAIEnvironment
+
+client = SmallestAI(
+ api_key="",
+ environment=SmallestAIEnvironment.PRODUCTION,
+)
+
+client.atoms.billing.get_invoice_pdf(
+ invoice_id="invoiceId",
+)
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**invoice_id:** `str` — Stripe invoice ID (e.g. `in_1THiCSRwh8g1U6dfOcUtTdq9`).
+
+
+
+
+
+-
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
diff --git a/src/smallestai/atoms/__init__.py b/src/smallestai/atoms/__init__.py
index c812c10b..f519704a 100644
--- a/src/smallestai/atoms/__init__.py
+++ b/src/smallestai/atoms/__init__.py
@@ -54,7 +54,21 @@
AnalyticsTrendMetric,
ApiResponse,
BadRequestErrorBody,
+ BadRequestErrorBodyError,
BadRequestErrorResponse,
+ BillingBalanceResponse,
+ BillingBalanceResponseData,
+ BillingInvoiceListResponse,
+ BillingInvoicePdfResponse,
+ BillingInvoicePdfResponseData,
+ BillingLedgerEntry,
+ BillingLedgerEntryTransactionType,
+ BillingLedgerResponse,
+ BillingLedgerResponseData,
+ BillingLedgerResponseDataPeriod,
+ BillingLedgerResponseDataSummary,
+ BillingUsageBreakdownResponse,
+ BillingUsageBreakdownResponseData,
Branch,
BranchStatus,
BranchSummary,
@@ -93,6 +107,7 @@
LockedErrorResponse,
LockedErrorResponseErrorType,
NotFoundErrorBody,
+ NotFoundErrorBodyError,
PostCallAnalyticsConfig,
PostCallAnalyticsConfigSuccessMetricsItem,
PostCallAnalyticsConfigSuccessMetricsItemSuccessMetricType,
@@ -199,6 +214,7 @@
agents,
analytics,
audience,
+ billing,
call_actions,
calls,
campaigns,
@@ -429,6 +445,7 @@
PostAudienceResponse,
PostAudienceResponseData,
)
+ from .billing import GetLedgerBillingRequestScope, GetLedgerBillingRequestType
from .call_actions import (
CreateCallActionRequestActionType,
CreateCallActionRequestCategory,
@@ -705,7 +722,21 @@
"BadGatewayError": ".errors",
"BadRequestError": ".errors",
"BadRequestErrorBody": ".types",
+ "BadRequestErrorBodyError": ".types",
"BadRequestErrorResponse": ".types",
+ "BillingBalanceResponse": ".types",
+ "BillingBalanceResponseData": ".types",
+ "BillingInvoiceListResponse": ".types",
+ "BillingInvoicePdfResponse": ".types",
+ "BillingInvoicePdfResponseData": ".types",
+ "BillingLedgerEntry": ".types",
+ "BillingLedgerEntryTransactionType": ".types",
+ "BillingLedgerResponse": ".types",
+ "BillingLedgerResponseData": ".types",
+ "BillingLedgerResponseDataPeriod": ".types",
+ "BillingLedgerResponseDataSummary": ".types",
+ "BillingUsageBreakdownResponse": ".types",
+ "BillingUsageBreakdownResponseData": ".types",
"Branch": ".types",
"BranchStatus": ".types",
"BranchSummary": ".types",
@@ -922,6 +953,8 @@
"GetKnowledgebaseIdItemsResponse": ".knowledge_base",
"GetKnowledgebaseIdScrapedUrlsResponse": ".knowledge_base",
"GetKnowledgebaseIdScrapedUrlsResponseDataItem": ".knowledge_base",
+ "GetLedgerBillingRequestScope": ".billing",
+ "GetLedgerBillingRequestType": ".billing",
"GetOrganizationResponse": ".organization",
"GetOrganizationResponseData": ".organization",
"GetOrganizationResponseDataMembersItem": ".organization",
@@ -1039,6 +1072,7 @@
"ModifyWebEngageIntegrationResponse": ".integrations",
"NotFoundError": ".errors",
"NotFoundErrorBody": ".types",
+ "NotFoundErrorBodyError": ".types",
"PauseCampaignsResponse": ".campaigns",
"PostAgentAgentIdWebhookSubscriptionsRequestEventTypesItem": ".webhooks",
"PostAgentAgentIdWebhookSubscriptionsResponse": ".webhooks",
@@ -1226,6 +1260,7 @@
"agents": ".agents",
"analytics": ".analytics",
"audience": ".audience",
+ "billing": ".billing",
"call_actions": ".call_actions",
"calls": ".calls",
"campaigns": ".campaigns",
@@ -1323,7 +1358,21 @@ def __dir__():
"BadGatewayError",
"BadRequestError",
"BadRequestErrorBody",
+ "BadRequestErrorBodyError",
"BadRequestErrorResponse",
+ "BillingBalanceResponse",
+ "BillingBalanceResponseData",
+ "BillingInvoiceListResponse",
+ "BillingInvoicePdfResponse",
+ "BillingInvoicePdfResponseData",
+ "BillingLedgerEntry",
+ "BillingLedgerEntryTransactionType",
+ "BillingLedgerResponse",
+ "BillingLedgerResponseData",
+ "BillingLedgerResponseDataPeriod",
+ "BillingLedgerResponseDataSummary",
+ "BillingUsageBreakdownResponse",
+ "BillingUsageBreakdownResponseData",
"Branch",
"BranchStatus",
"BranchSummary",
@@ -1540,6 +1589,8 @@ def __dir__():
"GetKnowledgebaseIdItemsResponse",
"GetKnowledgebaseIdScrapedUrlsResponse",
"GetKnowledgebaseIdScrapedUrlsResponseDataItem",
+ "GetLedgerBillingRequestScope",
+ "GetLedgerBillingRequestType",
"GetOrganizationResponse",
"GetOrganizationResponseData",
"GetOrganizationResponseDataMembersItem",
@@ -1657,6 +1708,7 @@ def __dir__():
"ModifyWebEngageIntegrationResponse",
"NotFoundError",
"NotFoundErrorBody",
+ "NotFoundErrorBodyError",
"PauseCampaignsResponse",
"PostAgentAgentIdWebhookSubscriptionsRequestEventTypesItem",
"PostAgentAgentIdWebhookSubscriptionsResponse",
@@ -1844,6 +1896,7 @@ def __dir__():
"agents",
"analytics",
"audience",
+ "billing",
"call_actions",
"calls",
"campaigns",
diff --git a/src/smallestai/atoms/billing/__init__.py b/src/smallestai/atoms/billing/__init__.py
new file mode 100644
index 00000000..c7898fd6
--- /dev/null
+++ b/src/smallestai/atoms/billing/__init__.py
@@ -0,0 +1,37 @@
+# This file was auto-generated by Fern from our API Definition.
+
+# isort: skip_file
+
+import typing
+from importlib import import_module
+
+if typing.TYPE_CHECKING:
+ from .types import GetLedgerBillingRequestScope, GetLedgerBillingRequestType
+_dynamic_imports: typing.Dict[str, str] = {
+ "GetLedgerBillingRequestScope": ".types",
+ "GetLedgerBillingRequestType": ".types",
+}
+
+
+def __getattr__(attr_name: str) -> typing.Any:
+ module_name = _dynamic_imports.get(attr_name)
+ if module_name is None:
+ raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}")
+ try:
+ module = import_module(module_name, __package__)
+ if module_name == f".{attr_name}":
+ return module
+ else:
+ return getattr(module, attr_name)
+ except ImportError as e:
+ raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e
+ except AttributeError as e:
+ raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e
+
+
+def __dir__():
+ lazy_attrs = list(_dynamic_imports.keys())
+ return sorted(lazy_attrs)
+
+
+__all__ = ["GetLedgerBillingRequestScope", "GetLedgerBillingRequestType"]
diff --git a/src/smallestai/atoms/billing/client.py b/src/smallestai/atoms/billing/client.py
new file mode 100644
index 00000000..eebcce73
--- /dev/null
+++ b/src/smallestai/atoms/billing/client.py
@@ -0,0 +1,518 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import datetime as dt
+import typing
+
+from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
+from ...core.request_options import RequestOptions
+from ..types.billing_balance_response import BillingBalanceResponse
+from ..types.billing_invoice_list_response import BillingInvoiceListResponse
+from ..types.billing_invoice_pdf_response import BillingInvoicePdfResponse
+from ..types.billing_ledger_response import BillingLedgerResponse
+from ..types.billing_usage_breakdown_response import BillingUsageBreakdownResponse
+from .raw_client import AsyncRawBillingClient, RawBillingClient
+from .types.get_ledger_billing_request_scope import GetLedgerBillingRequestScope
+from .types.get_ledger_billing_request_type import GetLedgerBillingRequestType
+
+
+class BillingClient:
+ def __init__(self, *, client_wrapper: SyncClientWrapper):
+ self._raw_client = RawBillingClient(client_wrapper=client_wrapper)
+
+ @property
+ def with_raw_response(self) -> RawBillingClient:
+ """
+ Retrieves a raw implementation of this client that returns raw responses.
+
+ Returns
+ -------
+ RawBillingClient
+ """
+ return self._raw_client
+
+ def get_balance(self, *, request_options: typing.Optional[RequestOptions] = None) -> BillingBalanceResponse:
+ """
+ Returns the organization's current credit balance in USD plus the
+ current plan identifier. Organization is resolved from the API key,
+ so no `X-Organization-Id` header is required.
+
+ Parameters
+ ----------
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ BillingBalanceResponse
+ Current balance for the caller's organization.
+
+ Examples
+ --------
+ from smallestai import SmallestAI
+
+ client = SmallestAI(
+ api_key="YOUR_API_KEY",
+ )
+ client.atoms.billing.get_balance()
+ """
+ _response = self._raw_client.get_balance(request_options=request_options)
+ return _response.data
+
+ def get_ledger(
+ self,
+ *,
+ limit: typing.Optional[int] = None,
+ offset: typing.Optional[int] = None,
+ from_: typing.Optional[dt.datetime] = None,
+ to: typing.Optional[dt.datetime] = None,
+ type: typing.Optional[GetLedgerBillingRequestType] = None,
+ scope: typing.Optional[GetLedgerBillingRequestScope] = None,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> BillingLedgerResponse:
+ """
+ Paginated credit-ledger transaction history. Ledger reads are served
+ from ClickHouse; a rare storage-tier outage returns an empty
+ `transactions` array rather than a 5xx.
+
+ **Window rules**
+
+ - `from` defaults to `to - 7 days`, `to` defaults to now.
+ - The span between `from` and `to` cannot exceed **90 days**. A wider
+ span returns 400. Page through longer periods by making multiple
+ calls with shifted `from`/`to`.
+ - `from` cannot be earlier than **2026-03-02T00:00:00Z**. Older
+ historical data is not available via API; contact support for bulk
+ exports.
+ - `from > to` returns 400.
+
+ **Filters**
+
+ - `type` filters to a single transaction type. `PAYMENTS` is a
+ virtual filter that returns both `CREDIT_PURCHASE` and
+ `AUTO_RELOAD` rows.
+ - `scope` filters `USAGE_DEDUCTION` rows by product category.
+
+ Parameters
+ ----------
+ limit : typing.Optional[int]
+ Page size (1–100).
+
+ offset : typing.Optional[int]
+ Offset for pagination.
+
+ from_ : typing.Optional[dt.datetime]
+ Lower bound of the query window (ISO 8601, `Z`-suffixed UTC recommended). Defaults to seven days before `to`. Cannot be earlier than `2026-03-02T00:00:00Z`.
+
+ to : typing.Optional[dt.datetime]
+ Upper bound of the query window (ISO 8601, `Z`-suffixed UTC recommended). Defaults to now.
+
+ type : typing.Optional[GetLedgerBillingRequestType]
+ Filter to a single transaction type. `PAYMENTS` is a virtual filter that returns purchase-related rows (`CREDIT_PURCHASE` + `AUTO_RELOAD`).
+
+ scope : typing.Optional[GetLedgerBillingRequestScope]
+ Filter by spend category. Applies only to `USAGE_DEDUCTION` rows.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ BillingLedgerResponse
+ Paginated ledger with summary + query window echo.
+
+ Examples
+ --------
+ import datetime
+
+ from smallestai import SmallestAI
+
+ client = SmallestAI(
+ api_key="YOUR_API_KEY",
+ )
+ client.atoms.billing.get_ledger(
+ from_=datetime.datetime.fromisoformat(
+ "2026-07-01 00:00:00+00:00",
+ ),
+ to=datetime.datetime.fromisoformat(
+ "2026-07-28 00:00:00+00:00",
+ ),
+ )
+ """
+ _response = self._raw_client.get_ledger(
+ limit=limit, offset=offset, from_=from_, to=to, type=type, scope=scope, request_options=request_options
+ )
+ return _response.data
+
+ def get_usage_breakdown(
+ self, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> BillingUsageBreakdownResponse:
+ """
+ Total credits spent so far, split across the three product scopes.
+ The window is a cumulative snapshot from **2026-03-02T00:00:00Z**
+ (the platform's usage-tracking start date) up to the current
+ instant. Served from ClickHouse. No query parameters.
+
+ Parameters
+ ----------
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ BillingUsageBreakdownResponse
+ Spend split by scope.
+
+ Examples
+ --------
+ from smallestai import SmallestAI
+
+ client = SmallestAI(
+ api_key="YOUR_API_KEY",
+ )
+ client.atoms.billing.get_usage_breakdown()
+ """
+ _response = self._raw_client.get_usage_breakdown(request_options=request_options)
+ return _response.data
+
+ def list_invoices(self, *, request_options: typing.Optional[RequestOptions] = None) -> BillingInvoiceListResponse:
+ """
+ Returns up to 20 of the most recent Stripe invoices for the caller's
+ organization. Each item is the raw Stripe `Invoice` object; use the
+ canonical Stripe reference at
+ [stripe.com/docs/api/invoices/object](https://stripe.com/docs/api/invoices/object)
+ for field-level semantics.
+
+ Organizations that have never been charged (free-tier only) return
+ an empty array.
+
+ Parameters
+ ----------
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ BillingInvoiceListResponse
+ Up to 20 recent Stripe invoices for the caller's organization.
+
+ Examples
+ --------
+ from smallestai import SmallestAI
+
+ client = SmallestAI(
+ api_key="YOUR_API_KEY",
+ )
+ client.atoms.billing.list_invoices()
+ """
+ _response = self._raw_client.list_invoices(request_options=request_options)
+ return _response.data
+
+ def get_invoice_pdf(
+ self, invoice_id: str, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> BillingInvoicePdfResponse:
+ """
+ Returns a Stripe-hosted PDF URL for the invoice. The URL is
+ short-lived; fetch fresh when you need to hand it to a user.
+
+ Requesting an invoice that does not belong to the caller's
+ organization returns 404 (not 403), so a foreign invoice ID cannot
+ be confirmed to exist.
+
+ Parameters
+ ----------
+ invoice_id : str
+ Stripe invoice ID (e.g. `in_1THiCSRwh8g1U6dfOcUtTdq9`).
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ BillingInvoicePdfResponse
+ Stripe-hosted PDF URL for the invoice.
+
+ Examples
+ --------
+ from smallestai import SmallestAI
+
+ client = SmallestAI(
+ api_key="YOUR_API_KEY",
+ )
+ client.atoms.billing.get_invoice_pdf(
+ invoice_id="invoiceId",
+ )
+ """
+ _response = self._raw_client.get_invoice_pdf(invoice_id, request_options=request_options)
+ return _response.data
+
+
+class AsyncBillingClient:
+ def __init__(self, *, client_wrapper: AsyncClientWrapper):
+ self._raw_client = AsyncRawBillingClient(client_wrapper=client_wrapper)
+
+ @property
+ def with_raw_response(self) -> AsyncRawBillingClient:
+ """
+ Retrieves a raw implementation of this client that returns raw responses.
+
+ Returns
+ -------
+ AsyncRawBillingClient
+ """
+ return self._raw_client
+
+ async def get_balance(self, *, request_options: typing.Optional[RequestOptions] = None) -> BillingBalanceResponse:
+ """
+ Returns the organization's current credit balance in USD plus the
+ current plan identifier. Organization is resolved from the API key,
+ so no `X-Organization-Id` header is required.
+
+ Parameters
+ ----------
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ BillingBalanceResponse
+ Current balance for the caller's organization.
+
+ Examples
+ --------
+ import asyncio
+
+ from smallestai import AsyncSmallestAI
+
+ client = AsyncSmallestAI(
+ api_key="YOUR_API_KEY",
+ )
+
+
+ async def main() -> None:
+ await client.atoms.billing.get_balance()
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.get_balance(request_options=request_options)
+ return _response.data
+
+ async def get_ledger(
+ self,
+ *,
+ limit: typing.Optional[int] = None,
+ offset: typing.Optional[int] = None,
+ from_: typing.Optional[dt.datetime] = None,
+ to: typing.Optional[dt.datetime] = None,
+ type: typing.Optional[GetLedgerBillingRequestType] = None,
+ scope: typing.Optional[GetLedgerBillingRequestScope] = None,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> BillingLedgerResponse:
+ """
+ Paginated credit-ledger transaction history. Ledger reads are served
+ from ClickHouse; a rare storage-tier outage returns an empty
+ `transactions` array rather than a 5xx.
+
+ **Window rules**
+
+ - `from` defaults to `to - 7 days`, `to` defaults to now.
+ - The span between `from` and `to` cannot exceed **90 days**. A wider
+ span returns 400. Page through longer periods by making multiple
+ calls with shifted `from`/`to`.
+ - `from` cannot be earlier than **2026-03-02T00:00:00Z**. Older
+ historical data is not available via API; contact support for bulk
+ exports.
+ - `from > to` returns 400.
+
+ **Filters**
+
+ - `type` filters to a single transaction type. `PAYMENTS` is a
+ virtual filter that returns both `CREDIT_PURCHASE` and
+ `AUTO_RELOAD` rows.
+ - `scope` filters `USAGE_DEDUCTION` rows by product category.
+
+ Parameters
+ ----------
+ limit : typing.Optional[int]
+ Page size (1–100).
+
+ offset : typing.Optional[int]
+ Offset for pagination.
+
+ from_ : typing.Optional[dt.datetime]
+ Lower bound of the query window (ISO 8601, `Z`-suffixed UTC recommended). Defaults to seven days before `to`. Cannot be earlier than `2026-03-02T00:00:00Z`.
+
+ to : typing.Optional[dt.datetime]
+ Upper bound of the query window (ISO 8601, `Z`-suffixed UTC recommended). Defaults to now.
+
+ type : typing.Optional[GetLedgerBillingRequestType]
+ Filter to a single transaction type. `PAYMENTS` is a virtual filter that returns purchase-related rows (`CREDIT_PURCHASE` + `AUTO_RELOAD`).
+
+ scope : typing.Optional[GetLedgerBillingRequestScope]
+ Filter by spend category. Applies only to `USAGE_DEDUCTION` rows.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ BillingLedgerResponse
+ Paginated ledger with summary + query window echo.
+
+ Examples
+ --------
+ import asyncio
+ import datetime
+
+ from smallestai import AsyncSmallestAI
+
+ client = AsyncSmallestAI(
+ api_key="YOUR_API_KEY",
+ )
+
+
+ async def main() -> None:
+ await client.atoms.billing.get_ledger(
+ from_=datetime.datetime.fromisoformat(
+ "2026-07-01 00:00:00+00:00",
+ ),
+ to=datetime.datetime.fromisoformat(
+ "2026-07-28 00:00:00+00:00",
+ ),
+ )
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.get_ledger(
+ limit=limit, offset=offset, from_=from_, to=to, type=type, scope=scope, request_options=request_options
+ )
+ return _response.data
+
+ async def get_usage_breakdown(
+ self, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> BillingUsageBreakdownResponse:
+ """
+ Total credits spent so far, split across the three product scopes.
+ The window is a cumulative snapshot from **2026-03-02T00:00:00Z**
+ (the platform's usage-tracking start date) up to the current
+ instant. Served from ClickHouse. No query parameters.
+
+ Parameters
+ ----------
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ BillingUsageBreakdownResponse
+ Spend split by scope.
+
+ Examples
+ --------
+ import asyncio
+
+ from smallestai import AsyncSmallestAI
+
+ client = AsyncSmallestAI(
+ api_key="YOUR_API_KEY",
+ )
+
+
+ async def main() -> None:
+ await client.atoms.billing.get_usage_breakdown()
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.get_usage_breakdown(request_options=request_options)
+ return _response.data
+
+ async def list_invoices(
+ self, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> BillingInvoiceListResponse:
+ """
+ Returns up to 20 of the most recent Stripe invoices for the caller's
+ organization. Each item is the raw Stripe `Invoice` object; use the
+ canonical Stripe reference at
+ [stripe.com/docs/api/invoices/object](https://stripe.com/docs/api/invoices/object)
+ for field-level semantics.
+
+ Organizations that have never been charged (free-tier only) return
+ an empty array.
+
+ Parameters
+ ----------
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ BillingInvoiceListResponse
+ Up to 20 recent Stripe invoices for the caller's organization.
+
+ Examples
+ --------
+ import asyncio
+
+ from smallestai import AsyncSmallestAI
+
+ client = AsyncSmallestAI(
+ api_key="YOUR_API_KEY",
+ )
+
+
+ async def main() -> None:
+ await client.atoms.billing.list_invoices()
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.list_invoices(request_options=request_options)
+ return _response.data
+
+ async def get_invoice_pdf(
+ self, invoice_id: str, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> BillingInvoicePdfResponse:
+ """
+ Returns a Stripe-hosted PDF URL for the invoice. The URL is
+ short-lived; fetch fresh when you need to hand it to a user.
+
+ Requesting an invoice that does not belong to the caller's
+ organization returns 404 (not 403), so a foreign invoice ID cannot
+ be confirmed to exist.
+
+ Parameters
+ ----------
+ invoice_id : str
+ Stripe invoice ID (e.g. `in_1THiCSRwh8g1U6dfOcUtTdq9`).
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ BillingInvoicePdfResponse
+ Stripe-hosted PDF URL for the invoice.
+
+ Examples
+ --------
+ import asyncio
+
+ from smallestai import AsyncSmallestAI
+
+ client = AsyncSmallestAI(
+ api_key="YOUR_API_KEY",
+ )
+
+
+ async def main() -> None:
+ await client.atoms.billing.get_invoice_pdf(
+ invoice_id="invoiceId",
+ )
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.get_invoice_pdf(invoice_id, request_options=request_options)
+ return _response.data
diff --git a/src/smallestai/atoms/billing/raw_client.py b/src/smallestai/atoms/billing/raw_client.py
new file mode 100644
index 00000000..3a24ec6e
--- /dev/null
+++ b/src/smallestai/atoms/billing/raw_client.py
@@ -0,0 +1,747 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import datetime as dt
+import typing
+from json.decoder import JSONDecodeError
+
+from ...core.api_error import ApiError
+from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
+from ...core.datetime_utils import serialize_datetime
+from ...core.http_response import AsyncHttpResponse, HttpResponse
+from ...core.jsonable_encoder import encode_path_param
+from ...core.parse_error import ParsingError
+from ...core.request_options import RequestOptions
+from ...core.unchecked_base_model import construct_type
+from ..errors.bad_request_error import BadRequestError
+from ..errors.not_found_error import NotFoundError
+from ..errors.unauthorized_error import UnauthorizedError
+from ..types.billing_balance_response import BillingBalanceResponse
+from ..types.billing_invoice_list_response import BillingInvoiceListResponse
+from ..types.billing_invoice_pdf_response import BillingInvoicePdfResponse
+from ..types.billing_ledger_response import BillingLedgerResponse
+from ..types.billing_usage_breakdown_response import BillingUsageBreakdownResponse
+from .types.get_ledger_billing_request_scope import GetLedgerBillingRequestScope
+from .types.get_ledger_billing_request_type import GetLedgerBillingRequestType
+from pydantic import ValidationError
+
+
+class RawBillingClient:
+ def __init__(self, *, client_wrapper: SyncClientWrapper):
+ self._client_wrapper = client_wrapper
+
+ def get_balance(
+ self, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> HttpResponse[BillingBalanceResponse]:
+ """
+ Returns the organization's current credit balance in USD plus the
+ current plan identifier. Organization is resolved from the API key,
+ so no `X-Organization-Id` header is required.
+
+ Parameters
+ ----------
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[BillingBalanceResponse]
+ Current balance for the caller's organization.
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "payment/v1/credits/balance",
+ base_url=self._client_wrapper.get_environment().payment,
+ method="GET",
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ BillingBalanceResponse,
+ construct_type(
+ type_=BillingBalanceResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return HttpResponse(response=_response, data=_data)
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ def get_ledger(
+ self,
+ *,
+ limit: typing.Optional[int] = None,
+ offset: typing.Optional[int] = None,
+ from_: typing.Optional[dt.datetime] = None,
+ to: typing.Optional[dt.datetime] = None,
+ type: typing.Optional[GetLedgerBillingRequestType] = None,
+ scope: typing.Optional[GetLedgerBillingRequestScope] = None,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> HttpResponse[BillingLedgerResponse]:
+ """
+ Paginated credit-ledger transaction history. Ledger reads are served
+ from ClickHouse; a rare storage-tier outage returns an empty
+ `transactions` array rather than a 5xx.
+
+ **Window rules**
+
+ - `from` defaults to `to - 7 days`, `to` defaults to now.
+ - The span between `from` and `to` cannot exceed **90 days**. A wider
+ span returns 400. Page through longer periods by making multiple
+ calls with shifted `from`/`to`.
+ - `from` cannot be earlier than **2026-03-02T00:00:00Z**. Older
+ historical data is not available via API; contact support for bulk
+ exports.
+ - `from > to` returns 400.
+
+ **Filters**
+
+ - `type` filters to a single transaction type. `PAYMENTS` is a
+ virtual filter that returns both `CREDIT_PURCHASE` and
+ `AUTO_RELOAD` rows.
+ - `scope` filters `USAGE_DEDUCTION` rows by product category.
+
+ Parameters
+ ----------
+ limit : typing.Optional[int]
+ Page size (1–100).
+
+ offset : typing.Optional[int]
+ Offset for pagination.
+
+ from_ : typing.Optional[dt.datetime]
+ Lower bound of the query window (ISO 8601, `Z`-suffixed UTC recommended). Defaults to seven days before `to`. Cannot be earlier than `2026-03-02T00:00:00Z`.
+
+ to : typing.Optional[dt.datetime]
+ Upper bound of the query window (ISO 8601, `Z`-suffixed UTC recommended). Defaults to now.
+
+ type : typing.Optional[GetLedgerBillingRequestType]
+ Filter to a single transaction type. `PAYMENTS` is a virtual filter that returns purchase-related rows (`CREDIT_PURCHASE` + `AUTO_RELOAD`).
+
+ scope : typing.Optional[GetLedgerBillingRequestScope]
+ Filter by spend category. Applies only to `USAGE_DEDUCTION` rows.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[BillingLedgerResponse]
+ Paginated ledger with summary + query window echo.
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "payment/v1/credits/ledger",
+ base_url=self._client_wrapper.get_environment().payment,
+ method="GET",
+ params={
+ "limit": limit,
+ "offset": offset,
+ "from": serialize_datetime(from_) if from_ is not None else None,
+ "to": serialize_datetime(to) if to is not None else None,
+ "type": type,
+ "scope": scope,
+ },
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ BillingLedgerResponse,
+ construct_type(
+ type_=BillingLedgerResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return HttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ def get_usage_breakdown(
+ self, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> HttpResponse[BillingUsageBreakdownResponse]:
+ """
+ Total credits spent so far, split across the three product scopes.
+ The window is a cumulative snapshot from **2026-03-02T00:00:00Z**
+ (the platform's usage-tracking start date) up to the current
+ instant. Served from ClickHouse. No query parameters.
+
+ Parameters
+ ----------
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[BillingUsageBreakdownResponse]
+ Spend split by scope.
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "payment/v1/credits/usage/breakdown",
+ base_url=self._client_wrapper.get_environment().payment,
+ method="GET",
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ BillingUsageBreakdownResponse,
+ construct_type(
+ type_=BillingUsageBreakdownResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return HttpResponse(response=_response, data=_data)
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ def list_invoices(
+ self, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> HttpResponse[BillingInvoiceListResponse]:
+ """
+ Returns up to 20 of the most recent Stripe invoices for the caller's
+ organization. Each item is the raw Stripe `Invoice` object; use the
+ canonical Stripe reference at
+ [stripe.com/docs/api/invoices/object](https://stripe.com/docs/api/invoices/object)
+ for field-level semantics.
+
+ Organizations that have never been charged (free-tier only) return
+ an empty array.
+
+ Parameters
+ ----------
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[BillingInvoiceListResponse]
+ Up to 20 recent Stripe invoices for the caller's organization.
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "payment/v1/invoices",
+ base_url=self._client_wrapper.get_environment().payment,
+ method="GET",
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ BillingInvoiceListResponse,
+ construct_type(
+ type_=BillingInvoiceListResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return HttpResponse(response=_response, data=_data)
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ def get_invoice_pdf(
+ self, invoice_id: str, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> HttpResponse[BillingInvoicePdfResponse]:
+ """
+ Returns a Stripe-hosted PDF URL for the invoice. The URL is
+ short-lived; fetch fresh when you need to hand it to a user.
+
+ Requesting an invoice that does not belong to the caller's
+ organization returns 404 (not 403), so a foreign invoice ID cannot
+ be confirmed to exist.
+
+ Parameters
+ ----------
+ invoice_id : str
+ Stripe invoice ID (e.g. `in_1THiCSRwh8g1U6dfOcUtTdq9`).
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[BillingInvoicePdfResponse]
+ Stripe-hosted PDF URL for the invoice.
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ f"payment/v1/invoices/{encode_path_param(invoice_id)}/pdf",
+ base_url=self._client_wrapper.get_environment().payment,
+ method="GET",
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ BillingInvoicePdfResponse,
+ construct_type(
+ type_=BillingInvoicePdfResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return HttpResponse(response=_response, data=_data)
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+
+class AsyncRawBillingClient:
+ def __init__(self, *, client_wrapper: AsyncClientWrapper):
+ self._client_wrapper = client_wrapper
+
+ async def get_balance(
+ self, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> AsyncHttpResponse[BillingBalanceResponse]:
+ """
+ Returns the organization's current credit balance in USD plus the
+ current plan identifier. Organization is resolved from the API key,
+ so no `X-Organization-Id` header is required.
+
+ Parameters
+ ----------
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ AsyncHttpResponse[BillingBalanceResponse]
+ Current balance for the caller's organization.
+ """
+ _response = await self._client_wrapper.httpx_client.request(
+ "payment/v1/credits/balance",
+ base_url=self._client_wrapper.get_environment().payment,
+ method="GET",
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ BillingBalanceResponse,
+ construct_type(
+ type_=BillingBalanceResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return AsyncHttpResponse(response=_response, data=_data)
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ async def get_ledger(
+ self,
+ *,
+ limit: typing.Optional[int] = None,
+ offset: typing.Optional[int] = None,
+ from_: typing.Optional[dt.datetime] = None,
+ to: typing.Optional[dt.datetime] = None,
+ type: typing.Optional[GetLedgerBillingRequestType] = None,
+ scope: typing.Optional[GetLedgerBillingRequestScope] = None,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> AsyncHttpResponse[BillingLedgerResponse]:
+ """
+ Paginated credit-ledger transaction history. Ledger reads are served
+ from ClickHouse; a rare storage-tier outage returns an empty
+ `transactions` array rather than a 5xx.
+
+ **Window rules**
+
+ - `from` defaults to `to - 7 days`, `to` defaults to now.
+ - The span between `from` and `to` cannot exceed **90 days**. A wider
+ span returns 400. Page through longer periods by making multiple
+ calls with shifted `from`/`to`.
+ - `from` cannot be earlier than **2026-03-02T00:00:00Z**. Older
+ historical data is not available via API; contact support for bulk
+ exports.
+ - `from > to` returns 400.
+
+ **Filters**
+
+ - `type` filters to a single transaction type. `PAYMENTS` is a
+ virtual filter that returns both `CREDIT_PURCHASE` and
+ `AUTO_RELOAD` rows.
+ - `scope` filters `USAGE_DEDUCTION` rows by product category.
+
+ Parameters
+ ----------
+ limit : typing.Optional[int]
+ Page size (1–100).
+
+ offset : typing.Optional[int]
+ Offset for pagination.
+
+ from_ : typing.Optional[dt.datetime]
+ Lower bound of the query window (ISO 8601, `Z`-suffixed UTC recommended). Defaults to seven days before `to`. Cannot be earlier than `2026-03-02T00:00:00Z`.
+
+ to : typing.Optional[dt.datetime]
+ Upper bound of the query window (ISO 8601, `Z`-suffixed UTC recommended). Defaults to now.
+
+ type : typing.Optional[GetLedgerBillingRequestType]
+ Filter to a single transaction type. `PAYMENTS` is a virtual filter that returns purchase-related rows (`CREDIT_PURCHASE` + `AUTO_RELOAD`).
+
+ scope : typing.Optional[GetLedgerBillingRequestScope]
+ Filter by spend category. Applies only to `USAGE_DEDUCTION` rows.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ AsyncHttpResponse[BillingLedgerResponse]
+ Paginated ledger with summary + query window echo.
+ """
+ _response = await self._client_wrapper.httpx_client.request(
+ "payment/v1/credits/ledger",
+ base_url=self._client_wrapper.get_environment().payment,
+ method="GET",
+ params={
+ "limit": limit,
+ "offset": offset,
+ "from": serialize_datetime(from_) if from_ is not None else None,
+ "to": serialize_datetime(to) if to is not None else None,
+ "type": type,
+ "scope": scope,
+ },
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ BillingLedgerResponse,
+ construct_type(
+ type_=BillingLedgerResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return AsyncHttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ async def get_usage_breakdown(
+ self, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> AsyncHttpResponse[BillingUsageBreakdownResponse]:
+ """
+ Total credits spent so far, split across the three product scopes.
+ The window is a cumulative snapshot from **2026-03-02T00:00:00Z**
+ (the platform's usage-tracking start date) up to the current
+ instant. Served from ClickHouse. No query parameters.
+
+ Parameters
+ ----------
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ AsyncHttpResponse[BillingUsageBreakdownResponse]
+ Spend split by scope.
+ """
+ _response = await self._client_wrapper.httpx_client.request(
+ "payment/v1/credits/usage/breakdown",
+ base_url=self._client_wrapper.get_environment().payment,
+ method="GET",
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ BillingUsageBreakdownResponse,
+ construct_type(
+ type_=BillingUsageBreakdownResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return AsyncHttpResponse(response=_response, data=_data)
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ async def list_invoices(
+ self, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> AsyncHttpResponse[BillingInvoiceListResponse]:
+ """
+ Returns up to 20 of the most recent Stripe invoices for the caller's
+ organization. Each item is the raw Stripe `Invoice` object; use the
+ canonical Stripe reference at
+ [stripe.com/docs/api/invoices/object](https://stripe.com/docs/api/invoices/object)
+ for field-level semantics.
+
+ Organizations that have never been charged (free-tier only) return
+ an empty array.
+
+ Parameters
+ ----------
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ AsyncHttpResponse[BillingInvoiceListResponse]
+ Up to 20 recent Stripe invoices for the caller's organization.
+ """
+ _response = await self._client_wrapper.httpx_client.request(
+ "payment/v1/invoices",
+ base_url=self._client_wrapper.get_environment().payment,
+ method="GET",
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ BillingInvoiceListResponse,
+ construct_type(
+ type_=BillingInvoiceListResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return AsyncHttpResponse(response=_response, data=_data)
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ async def get_invoice_pdf(
+ self, invoice_id: str, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> AsyncHttpResponse[BillingInvoicePdfResponse]:
+ """
+ Returns a Stripe-hosted PDF URL for the invoice. The URL is
+ short-lived; fetch fresh when you need to hand it to a user.
+
+ Requesting an invoice that does not belong to the caller's
+ organization returns 404 (not 403), so a foreign invoice ID cannot
+ be confirmed to exist.
+
+ Parameters
+ ----------
+ invoice_id : str
+ Stripe invoice ID (e.g. `in_1THiCSRwh8g1U6dfOcUtTdq9`).
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ AsyncHttpResponse[BillingInvoicePdfResponse]
+ Stripe-hosted PDF URL for the invoice.
+ """
+ _response = await self._client_wrapper.httpx_client.request(
+ f"payment/v1/invoices/{encode_path_param(invoice_id)}/pdf",
+ base_url=self._client_wrapper.get_environment().payment,
+ method="GET",
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ BillingInvoicePdfResponse,
+ construct_type(
+ type_=BillingInvoicePdfResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return AsyncHttpResponse(response=_response, data=_data)
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
diff --git a/src/smallestai/atoms/billing/types/__init__.py b/src/smallestai/atoms/billing/types/__init__.py
new file mode 100644
index 00000000..a692d8c9
--- /dev/null
+++ b/src/smallestai/atoms/billing/types/__init__.py
@@ -0,0 +1,38 @@
+# This file was auto-generated by Fern from our API Definition.
+
+# isort: skip_file
+
+import typing
+from importlib import import_module
+
+if typing.TYPE_CHECKING:
+ from .get_ledger_billing_request_scope import GetLedgerBillingRequestScope
+ from .get_ledger_billing_request_type import GetLedgerBillingRequestType
+_dynamic_imports: typing.Dict[str, str] = {
+ "GetLedgerBillingRequestScope": ".get_ledger_billing_request_scope",
+ "GetLedgerBillingRequestType": ".get_ledger_billing_request_type",
+}
+
+
+def __getattr__(attr_name: str) -> typing.Any:
+ module_name = _dynamic_imports.get(attr_name)
+ if module_name is None:
+ raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}")
+ try:
+ module = import_module(module_name, __package__)
+ if module_name == f".{attr_name}":
+ return module
+ else:
+ return getattr(module, attr_name)
+ except ImportError as e:
+ raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e
+ except AttributeError as e:
+ raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e
+
+
+def __dir__():
+ lazy_attrs = list(_dynamic_imports.keys())
+ return sorted(lazy_attrs)
+
+
+__all__ = ["GetLedgerBillingRequestScope", "GetLedgerBillingRequestType"]
diff --git a/src/smallestai/atoms/billing/types/get_ledger_billing_request_scope.py b/src/smallestai/atoms/billing/types/get_ledger_billing_request_scope.py
new file mode 100644
index 00000000..48a4e50c
--- /dev/null
+++ b/src/smallestai/atoms/billing/types/get_ledger_billing_request_scope.py
@@ -0,0 +1,5 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+GetLedgerBillingRequestScope = typing.Union[typing.Literal["platform", "voice_ai", "voice_models"], typing.Any]
diff --git a/src/smallestai/atoms/billing/types/get_ledger_billing_request_type.py b/src/smallestai/atoms/billing/types/get_ledger_billing_request_type.py
new file mode 100644
index 00000000..51f02ca4
--- /dev/null
+++ b/src/smallestai/atoms/billing/types/get_ledger_billing_request_type.py
@@ -0,0 +1,17 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+GetLedgerBillingRequestType = typing.Union[
+ typing.Literal[
+ "SIGNUP_BONUS",
+ "CREDIT_PURCHASE",
+ "AUTO_RELOAD",
+ "USAGE_DEDUCTION",
+ "ADMIN_ADJUSTMENT",
+ "COUPON_CREDIT",
+ "MIGRATION",
+ "PAYMENTS",
+ ],
+ typing.Any,
+]
diff --git a/src/smallestai/atoms/calls/types/get_calls_response_data_transcript_item.py b/src/smallestai/atoms/calls/types/get_calls_response_data_transcript_item.py
index c6543ec8..74cb927f 100644
--- a/src/smallestai/atoms/calls/types/get_calls_response_data_transcript_item.py
+++ b/src/smallestai/atoms/calls/types/get_calls_response_data_transcript_item.py
@@ -19,9 +19,9 @@ class GetCallsResponseDataTranscriptItem(UncheckedBaseModel):
The spoken text for this turn
"""
- timestamp: typing.Optional[float] = pydantic.Field(default=None)
+ timestamp: typing.Optional[str] = pydantic.Field(default=None)
"""
- Timestamp offset in milliseconds from call start
+ ISO-8601 timestamp of the turn (e.g. 2026-07-28T15:46:12.472Z)
"""
if IS_PYDANTIC_V2:
diff --git a/src/smallestai/atoms/client.py b/src/smallestai/atoms/client.py
index ffe5c889..96f20119 100644
--- a/src/smallestai/atoms/client.py
+++ b/src/smallestai/atoms/client.py
@@ -16,6 +16,7 @@
from .agents.client import AgentsClient, AsyncAgentsClient
from .analytics.client import AnalyticsClient, AsyncAnalyticsClient
from .audience.client import AsyncAudienceClient, AudienceClient
+ from .billing.client import AsyncBillingClient, BillingClient
from .call_actions.client import AsyncCallActionsClient, CallActionsClient
from .calls.client import AsyncCallsClient, CallsClient
from .campaigns.client import AsyncCampaignsClient, CampaignsClient
@@ -67,6 +68,7 @@ def __init__(self, *, client_wrapper: SyncClientWrapper):
self._concurrency: typing.Optional[ConcurrencyClient] = None
self._disposition_metric_templates: typing.Optional[DispositionMetricTemplatesClient] = None
self._dnc: typing.Optional[DncClient] = None
+ self._billing: typing.Optional[BillingClient] = None
@property
def with_raw_response(self) -> RawAtomsClient:
@@ -279,6 +281,14 @@ def dnc(self):
self._dnc = DncClient(client_wrapper=self._client_wrapper)
return self._dnc
+ @property
+ def billing(self):
+ if self._billing is None:
+ from .billing.client import BillingClient # noqa: E402
+
+ self._billing = BillingClient(client_wrapper=self._client_wrapper)
+ return self._billing
+
class AsyncAtomsClient:
def __init__(self, *, client_wrapper: AsyncClientWrapper):
@@ -309,6 +319,7 @@ def __init__(self, *, client_wrapper: AsyncClientWrapper):
self._concurrency: typing.Optional[AsyncConcurrencyClient] = None
self._disposition_metric_templates: typing.Optional[AsyncDispositionMetricTemplatesClient] = None
self._dnc: typing.Optional[AsyncDncClient] = None
+ self._billing: typing.Optional[AsyncBillingClient] = None
@property
def with_raw_response(self) -> AsyncRawAtomsClient:
@@ -522,3 +533,11 @@ def dnc(self):
self._dnc = AsyncDncClient(client_wrapper=self._client_wrapper)
return self._dnc
+
+ @property
+ def billing(self):
+ if self._billing is None:
+ from .billing.client import AsyncBillingClient # noqa: E402
+
+ self._billing = AsyncBillingClient(client_wrapper=self._client_wrapper)
+ return self._billing
diff --git a/src/smallestai/atoms/types/__init__.py b/src/smallestai/atoms/types/__init__.py
index b7ed31ee..b66c2646 100644
--- a/src/smallestai/atoms/types/__init__.py
+++ b/src/smallestai/atoms/types/__init__.py
@@ -53,7 +53,21 @@
from .analytics_trend_metric import AnalyticsTrendMetric
from .api_response import ApiResponse
from .bad_request_error_body import BadRequestErrorBody
+ from .bad_request_error_body_error import BadRequestErrorBodyError
from .bad_request_error_response import BadRequestErrorResponse
+ from .billing_balance_response import BillingBalanceResponse
+ from .billing_balance_response_data import BillingBalanceResponseData
+ from .billing_invoice_list_response import BillingInvoiceListResponse
+ from .billing_invoice_pdf_response import BillingInvoicePdfResponse
+ from .billing_invoice_pdf_response_data import BillingInvoicePdfResponseData
+ from .billing_ledger_entry import BillingLedgerEntry
+ from .billing_ledger_entry_transaction_type import BillingLedgerEntryTransactionType
+ from .billing_ledger_response import BillingLedgerResponse
+ from .billing_ledger_response_data import BillingLedgerResponseData
+ from .billing_ledger_response_data_period import BillingLedgerResponseDataPeriod
+ from .billing_ledger_response_data_summary import BillingLedgerResponseDataSummary
+ from .billing_usage_breakdown_response import BillingUsageBreakdownResponse
+ from .billing_usage_breakdown_response_data import BillingUsageBreakdownResponseData
from .branch import Branch
from .branch_status import BranchStatus
from .branch_summary import BranchSummary
@@ -92,6 +106,7 @@
from .locked_error_response import LockedErrorResponse
from .locked_error_response_error_type import LockedErrorResponseErrorType
from .not_found_error_body import NotFoundErrorBody
+ from .not_found_error_body_error import NotFoundErrorBodyError
from .post_call_analytics_config import PostCallAnalyticsConfig
from .post_call_analytics_config_success_metrics_item import PostCallAnalyticsConfigSuccessMetricsItem
from .post_call_analytics_config_success_metrics_item_success_metric_type import (
@@ -226,7 +241,21 @@
"AnalyticsTrendMetric": ".analytics_trend_metric",
"ApiResponse": ".api_response",
"BadRequestErrorBody": ".bad_request_error_body",
+ "BadRequestErrorBodyError": ".bad_request_error_body_error",
"BadRequestErrorResponse": ".bad_request_error_response",
+ "BillingBalanceResponse": ".billing_balance_response",
+ "BillingBalanceResponseData": ".billing_balance_response_data",
+ "BillingInvoiceListResponse": ".billing_invoice_list_response",
+ "BillingInvoicePdfResponse": ".billing_invoice_pdf_response",
+ "BillingInvoicePdfResponseData": ".billing_invoice_pdf_response_data",
+ "BillingLedgerEntry": ".billing_ledger_entry",
+ "BillingLedgerEntryTransactionType": ".billing_ledger_entry_transaction_type",
+ "BillingLedgerResponse": ".billing_ledger_response",
+ "BillingLedgerResponseData": ".billing_ledger_response_data",
+ "BillingLedgerResponseDataPeriod": ".billing_ledger_response_data_period",
+ "BillingLedgerResponseDataSummary": ".billing_ledger_response_data_summary",
+ "BillingUsageBreakdownResponse": ".billing_usage_breakdown_response",
+ "BillingUsageBreakdownResponseData": ".billing_usage_breakdown_response_data",
"Branch": ".branch",
"BranchStatus": ".branch_status",
"BranchSummary": ".branch_summary",
@@ -265,6 +294,7 @@
"LockedErrorResponse": ".locked_error_response",
"LockedErrorResponseErrorType": ".locked_error_response_error_type",
"NotFoundErrorBody": ".not_found_error_body",
+ "NotFoundErrorBodyError": ".not_found_error_body_error",
"PostCallAnalyticsConfig": ".post_call_analytics_config",
"PostCallAnalyticsConfigSuccessMetricsItem": ".post_call_analytics_config_success_metrics_item",
"PostCallAnalyticsConfigSuccessMetricsItemSuccessMetricType": ".post_call_analytics_config_success_metrics_item_success_metric_type",
@@ -421,7 +451,21 @@ def __dir__():
"AnalyticsTrendMetric",
"ApiResponse",
"BadRequestErrorBody",
+ "BadRequestErrorBodyError",
"BadRequestErrorResponse",
+ "BillingBalanceResponse",
+ "BillingBalanceResponseData",
+ "BillingInvoiceListResponse",
+ "BillingInvoicePdfResponse",
+ "BillingInvoicePdfResponseData",
+ "BillingLedgerEntry",
+ "BillingLedgerEntryTransactionType",
+ "BillingLedgerResponse",
+ "BillingLedgerResponseData",
+ "BillingLedgerResponseDataPeriod",
+ "BillingLedgerResponseDataSummary",
+ "BillingUsageBreakdownResponse",
+ "BillingUsageBreakdownResponseData",
"Branch",
"BranchStatus",
"BranchSummary",
@@ -460,6 +504,7 @@ def __dir__():
"LockedErrorResponse",
"LockedErrorResponseErrorType",
"NotFoundErrorBody",
+ "NotFoundErrorBodyError",
"PostCallAnalyticsConfig",
"PostCallAnalyticsConfigSuccessMetricsItem",
"PostCallAnalyticsConfigSuccessMetricsItemSuccessMetricType",
diff --git a/src/smallestai/atoms/types/bad_request_error_body.py b/src/smallestai/atoms/types/bad_request_error_body.py
index ad9708f7..e1913d82 100644
--- a/src/smallestai/atoms/types/bad_request_error_body.py
+++ b/src/smallestai/atoms/types/bad_request_error_body.py
@@ -5,11 +5,12 @@
import pydantic
from ...core.pydantic_utilities import IS_PYDANTIC_V2
from ...core.unchecked_base_model import UncheckedBaseModel
+from .bad_request_error_body_error import BadRequestErrorBodyError
class BadRequestErrorBody(UncheckedBaseModel):
- status: typing.Optional[bool] = None
- errors: typing.Optional[typing.List[str]] = None
+ success: typing.Optional[bool] = None
+ error: typing.Optional[BadRequestErrorBodyError] = None
if IS_PYDANTIC_V2:
model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
diff --git a/src/smallestai/atoms/types/bad_request_error_body_error.py b/src/smallestai/atoms/types/bad_request_error_body_error.py
new file mode 100644
index 00000000..b4451b95
--- /dev/null
+++ b/src/smallestai/atoms/types/bad_request_error_body_error.py
@@ -0,0 +1,21 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.unchecked_base_model import UncheckedBaseModel
+
+
+class BadRequestErrorBodyError(UncheckedBaseModel):
+ code: typing.Optional[str] = None
+ message: typing.Optional[str] = None
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/smallestai/atoms/types/billing_balance_response.py b/src/smallestai/atoms/types/billing_balance_response.py
new file mode 100644
index 00000000..2c4c1dec
--- /dev/null
+++ b/src/smallestai/atoms/types/billing_balance_response.py
@@ -0,0 +1,22 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.unchecked_base_model import UncheckedBaseModel
+from .billing_balance_response_data import BillingBalanceResponseData
+
+
+class BillingBalanceResponse(UncheckedBaseModel):
+ success: bool
+ data: BillingBalanceResponseData
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/smallestai/atoms/types/billing_balance_response_data.py b/src/smallestai/atoms/types/billing_balance_response_data.py
new file mode 100644
index 00000000..cf98217e
--- /dev/null
+++ b/src/smallestai/atoms/types/billing_balance_response_data.py
@@ -0,0 +1,41 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+import typing_extensions
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.serialization import FieldMetadata
+from ...core.unchecked_base_model import UncheckedBaseModel
+
+
+class BillingBalanceResponseData(UncheckedBaseModel):
+ credit_balance: typing_extensions.Annotated[
+ float,
+ FieldMetadata(alias="creditBalance"),
+ pydantic.Field(
+ alias="creditBalance",
+ description="Current credit balance in USD. May be negative if the account has spent past its balance and is on a plan that permits overdraw.",
+ ),
+ ]
+ plan_id: typing_extensions.Annotated[
+ str,
+ FieldMetadata(alias="planId"),
+ pydantic.Field(alias="planId", description="Identifier for the plan the organization is on."),
+ ]
+ is_enterprise: typing_extensions.Annotated[
+ bool,
+ FieldMetadata(alias="isEnterprise"),
+ pydantic.Field(
+ alias="isEnterprise", description="True when the organization is on an enterprise plan (billed offline)."
+ ),
+ ]
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/smallestai/atoms/types/billing_invoice_list_response.py b/src/smallestai/atoms/types/billing_invoice_list_response.py
new file mode 100644
index 00000000..04dffa3a
--- /dev/null
+++ b/src/smallestai/atoms/types/billing_invoice_list_response.py
@@ -0,0 +1,24 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.unchecked_base_model import UncheckedBaseModel
+
+
+class BillingInvoiceListResponse(UncheckedBaseModel):
+ success: bool
+ data: typing.List[typing.Dict[str, typing.Any]] = pydantic.Field()
+ """
+ Up to 20 recent Stripe `Invoice` objects for the caller's organization. Fields follow the canonical Stripe shape. Keys most callers use: `id`, `status`, `amount_paid`, `amount_due`, `currency`, `created`, `hosted_invoice_url`, `invoice_pdf`, `number`.
+ """
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/smallestai/atoms/types/billing_invoice_pdf_response.py b/src/smallestai/atoms/types/billing_invoice_pdf_response.py
new file mode 100644
index 00000000..eaec093c
--- /dev/null
+++ b/src/smallestai/atoms/types/billing_invoice_pdf_response.py
@@ -0,0 +1,22 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.unchecked_base_model import UncheckedBaseModel
+from .billing_invoice_pdf_response_data import BillingInvoicePdfResponseData
+
+
+class BillingInvoicePdfResponse(UncheckedBaseModel):
+ success: bool
+ data: BillingInvoicePdfResponseData
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/smallestai/atoms/types/billing_invoice_pdf_response_data.py b/src/smallestai/atoms/types/billing_invoice_pdf_response_data.py
new file mode 100644
index 00000000..b9a60436
--- /dev/null
+++ b/src/smallestai/atoms/types/billing_invoice_pdf_response_data.py
@@ -0,0 +1,29 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+import typing_extensions
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.serialization import FieldMetadata
+from ...core.unchecked_base_model import UncheckedBaseModel
+
+
+class BillingInvoicePdfResponseData(UncheckedBaseModel):
+ pdf_url: typing_extensions.Annotated[
+ str,
+ FieldMetadata(alias="pdfUrl"),
+ pydantic.Field(
+ alias="pdfUrl",
+ description="Stripe-hosted URL to the invoice PDF. Short-lived; refetch when you need to hand it to a user.",
+ ),
+ ]
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/smallestai/atoms/types/billing_ledger_entry.py b/src/smallestai/atoms/types/billing_ledger_entry.py
new file mode 100644
index 00000000..cc5d93c0
--- /dev/null
+++ b/src/smallestai/atoms/types/billing_ledger_entry.py
@@ -0,0 +1,73 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import datetime as dt
+import typing
+
+import pydantic
+import typing_extensions
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.serialization import FieldMetadata
+from ...core.unchecked_base_model import UncheckedBaseModel
+from .billing_ledger_entry_transaction_type import BillingLedgerEntryTransactionType
+
+
+class BillingLedgerEntry(UncheckedBaseModel):
+ id: str = pydantic.Field()
+ """
+ Ledger row identifier.
+ """
+
+ transaction_type: typing_extensions.Annotated[
+ BillingLedgerEntryTransactionType,
+ FieldMetadata(alias="transactionType"),
+ pydantic.Field(alias="transactionType"),
+ ]
+ amount: float = pydantic.Field()
+ """
+ Signed credit delta in USD. Positive for additions (`CREDIT_PURCHASE`, `AUTO_RELOAD`, `COUPON_CREDIT`, `SIGNUP_BONUS`), negative for spend (`USAGE_DEDUCTION`).
+ """
+
+ balance_after: typing_extensions.Annotated[
+ float,
+ FieldMetadata(alias="balanceAfter"),
+ pydantic.Field(alias="balanceAfter", description="Balance after this row was applied."),
+ ]
+ feature_id: typing_extensions.Annotated[
+ typing.Optional[str],
+ FieldMetadata(alias="featureId"),
+ pydantic.Field(
+ alias="featureId",
+ description="Product feature that triggered the row (e.g. `tts.generate`, `waves.stt`). Only set on `USAGE_DEDUCTION`.",
+ ),
+ ] = None
+ reference_id: typing_extensions.Annotated[
+ typing.Optional[str],
+ FieldMetadata(alias="referenceId"),
+ pydantic.Field(
+ alias="referenceId", description="External reference tying this row to a purchase, coupon, or admin action."
+ ),
+ ] = None
+ description: typing.Optional[str] = None
+ created_at: typing_extensions.Annotated[
+ dt.datetime, FieldMetadata(alias="createdAt"), pydantic.Field(alias="createdAt")
+ ]
+ call_id: typing_extensions.Annotated[
+ typing.Optional[str],
+ FieldMetadata(alias="callId"),
+ pydantic.Field(alias="callId", description="Associated call ID, when the row was produced by a call."),
+ ] = None
+ agent_id: typing_extensions.Annotated[
+ typing.Optional[str], FieldMetadata(alias="agentId"), pydantic.Field(alias="agentId")
+ ] = None
+ request_id: typing_extensions.Annotated[
+ typing.Optional[str], FieldMetadata(alias="requestId"), pydantic.Field(alias="requestId")
+ ] = None
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/smallestai/atoms/types/billing_ledger_entry_transaction_type.py b/src/smallestai/atoms/types/billing_ledger_entry_transaction_type.py
new file mode 100644
index 00000000..7fb73a75
--- /dev/null
+++ b/src/smallestai/atoms/types/billing_ledger_entry_transaction_type.py
@@ -0,0 +1,16 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+BillingLedgerEntryTransactionType = typing.Union[
+ typing.Literal[
+ "SIGNUP_BONUS",
+ "CREDIT_PURCHASE",
+ "AUTO_RELOAD",
+ "USAGE_DEDUCTION",
+ "ADMIN_ADJUSTMENT",
+ "COUPON_CREDIT",
+ "MIGRATION",
+ ],
+ typing.Any,
+]
diff --git a/src/smallestai/atoms/types/billing_ledger_response.py b/src/smallestai/atoms/types/billing_ledger_response.py
new file mode 100644
index 00000000..1a1b166a
--- /dev/null
+++ b/src/smallestai/atoms/types/billing_ledger_response.py
@@ -0,0 +1,22 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.unchecked_base_model import UncheckedBaseModel
+from .billing_ledger_response_data import BillingLedgerResponseData
+
+
+class BillingLedgerResponse(UncheckedBaseModel):
+ success: bool
+ data: BillingLedgerResponseData
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/smallestai/atoms/types/billing_ledger_response_data.py b/src/smallestai/atoms/types/billing_ledger_response_data.py
new file mode 100644
index 00000000..ed30430a
--- /dev/null
+++ b/src/smallestai/atoms/types/billing_ledger_response_data.py
@@ -0,0 +1,37 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+import typing_extensions
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.serialization import FieldMetadata
+from ...core.unchecked_base_model import UncheckedBaseModel
+from .billing_ledger_entry import BillingLedgerEntry
+from .billing_ledger_response_data_period import BillingLedgerResponseDataPeriod
+from .billing_ledger_response_data_summary import BillingLedgerResponseDataSummary
+
+
+class BillingLedgerResponseData(UncheckedBaseModel):
+ transactions: typing.List[BillingLedgerEntry]
+ total: int = pydantic.Field()
+ """
+ Total row count matching the filters (across all pages).
+ """
+
+ has_more: typing_extensions.Annotated[
+ bool,
+ FieldMetadata(alias="hasMore"),
+ pydantic.Field(alias="hasMore", description="True when more rows exist beyond `offset + limit`."),
+ ]
+ summary: BillingLedgerResponseDataSummary
+ period: BillingLedgerResponseDataPeriod
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/smallestai/atoms/types/billing_ledger_response_data_period.py b/src/smallestai/atoms/types/billing_ledger_response_data_period.py
new file mode 100644
index 00000000..bcb46d4f
--- /dev/null
+++ b/src/smallestai/atoms/types/billing_ledger_response_data_period.py
@@ -0,0 +1,34 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import datetime as dt
+import typing
+
+import pydantic
+import typing_extensions
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.serialization import FieldMetadata
+from ...core.unchecked_base_model import UncheckedBaseModel
+
+
+class BillingLedgerResponseDataPeriod(UncheckedBaseModel):
+ from_: typing_extensions.Annotated[
+ dt.datetime,
+ FieldMetadata(alias="from"),
+ pydantic.Field(
+ alias="from",
+ description="Echo of the effective lower bound (defaults to seven days before `to` when not supplied).",
+ ),
+ ]
+ to: dt.datetime = pydantic.Field()
+ """
+ Echo of the effective upper bound (defaults to server-now when not supplied).
+ """
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/smallestai/atoms/types/billing_ledger_response_data_summary.py b/src/smallestai/atoms/types/billing_ledger_response_data_summary.py
new file mode 100644
index 00000000..7640eb5a
--- /dev/null
+++ b/src/smallestai/atoms/types/billing_ledger_response_data_summary.py
@@ -0,0 +1,36 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+import typing_extensions
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.serialization import FieldMetadata
+from ...core.unchecked_base_model import UncheckedBaseModel
+
+
+class BillingLedgerResponseDataSummary(UncheckedBaseModel):
+ total_credits_consumed: typing_extensions.Annotated[
+ float,
+ FieldMetadata(alias="totalCreditsConsumed"),
+ pydantic.Field(
+ alias="totalCreditsConsumed", description="Sum of `USAGE_DEDUCTION` magnitudes across the window."
+ ),
+ ]
+ total_credits_added: typing_extensions.Annotated[
+ float,
+ FieldMetadata(alias="totalCreditsAdded"),
+ pydantic.Field(
+ alias="totalCreditsAdded",
+ description="Sum of positive rows (purchases, auto-reloads, coupon credits, signup bonuses) across the window.",
+ ),
+ ]
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/smallestai/atoms/types/billing_usage_breakdown_response.py b/src/smallestai/atoms/types/billing_usage_breakdown_response.py
new file mode 100644
index 00000000..86480427
--- /dev/null
+++ b/src/smallestai/atoms/types/billing_usage_breakdown_response.py
@@ -0,0 +1,22 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.unchecked_base_model import UncheckedBaseModel
+from .billing_usage_breakdown_response_data import BillingUsageBreakdownResponseData
+
+
+class BillingUsageBreakdownResponse(UncheckedBaseModel):
+ success: bool
+ data: BillingUsageBreakdownResponseData
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/smallestai/atoms/types/billing_usage_breakdown_response_data.py b/src/smallestai/atoms/types/billing_usage_breakdown_response_data.py
new file mode 100644
index 00000000..0090c911
--- /dev/null
+++ b/src/smallestai/atoms/types/billing_usage_breakdown_response_data.py
@@ -0,0 +1,41 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+import typing_extensions
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.serialization import FieldMetadata
+from ...core.unchecked_base_model import UncheckedBaseModel
+
+
+class BillingUsageBreakdownResponseData(UncheckedBaseModel):
+ platform: float = pydantic.Field()
+ """
+ Credits spent on Atoms platform features (phone rental, telephony minutes, etc.).
+ """
+
+ voice_ai: typing_extensions.Annotated[
+ float,
+ FieldMetadata(alias="voiceAi"),
+ pydantic.Field(
+ alias="voiceAi", description="Credits spent on voice-agent runtime (LLM + TTS combined during voice calls)."
+ ),
+ ]
+ voice_models: typing_extensions.Annotated[
+ float,
+ FieldMetadata(alias="voiceModels"),
+ pydantic.Field(
+ alias="voiceModels",
+ description="Credits spent on standalone Waves model calls (Lightning TTS, Pulse STT, Electron LLM used outside the voice-agent pipeline).",
+ ),
+ ]
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/smallestai/atoms/types/not_found_error_body.py b/src/smallestai/atoms/types/not_found_error_body.py
index 374b0fc6..09b9a676 100644
--- a/src/smallestai/atoms/types/not_found_error_body.py
+++ b/src/smallestai/atoms/types/not_found_error_body.py
@@ -5,11 +5,12 @@
import pydantic
from ...core.pydantic_utilities import IS_PYDANTIC_V2
from ...core.unchecked_base_model import UncheckedBaseModel
+from .not_found_error_body_error import NotFoundErrorBodyError
class NotFoundErrorBody(UncheckedBaseModel):
- status: typing.Optional[bool] = None
- errors: typing.Optional[typing.List[str]] = None
+ success: typing.Optional[bool] = None
+ error: typing.Optional[NotFoundErrorBodyError] = None
if IS_PYDANTIC_V2:
model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
diff --git a/src/smallestai/atoms/types/not_found_error_body_error.py b/src/smallestai/atoms/types/not_found_error_body_error.py
new file mode 100644
index 00000000..0dcf1c4e
--- /dev/null
+++ b/src/smallestai/atoms/types/not_found_error_body_error.py
@@ -0,0 +1,21 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.unchecked_base_model import UncheckedBaseModel
+
+
+class NotFoundErrorBodyError(UncheckedBaseModel):
+ code: typing.Optional[str] = None
+ message: typing.Optional[str] = None
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/smallestai/environment.py b/src/smallestai/environment.py
index c5f76703..a3fa4a47 100644
--- a/src/smallestai/environment.py
+++ b/src/smallestai/environment.py
@@ -6,12 +6,16 @@
class SmallestAIEnvironment:
PRODUCTION: SmallestAIEnvironment
- def __init__(self, *, atoms: str, waves: str, waves_ws: str):
+ def __init__(self, *, atoms: str, waves: str, waves_ws: str, payment: str):
self.atoms = atoms
self.waves = waves
self.waves_ws = waves_ws
+ self.payment = payment
SmallestAIEnvironment.PRODUCTION = SmallestAIEnvironment(
- atoms="https://api.smallest.ai/atoms/v1", waves="https://api.smallest.ai", waves_ws="wss://api.smallest.ai"
+ atoms="https://api.smallest.ai/atoms/v1",
+ waves="https://api.smallest.ai",
+ waves_ws="wss://api.smallest.ai",
+ payment="https://api.smallest.ai",
)
diff --git a/tests/wire/conftest.py b/tests/wire/conftest.py
index 5646a37b..9fd029ef 100644
--- a/tests/wire/conftest.py
+++ b/tests/wire/conftest.py
@@ -45,13 +45,13 @@ def get_client(test_id: str) -> SmallestAI:
if _CLIENT_SUPPORTS_HEADERS:
return SmallestAI(
- environment=SmallestAIEnvironment(atoms=base_url, waves=base_url, waves_ws=base_url),
+ environment=SmallestAIEnvironment(atoms=base_url, waves=base_url, waves_ws=base_url, payment=base_url),
headers=test_headers,
api_key="test_token",
)
return SmallestAI(
- environment=SmallestAIEnvironment(atoms=base_url, waves=base_url, waves_ws=base_url),
+ environment=SmallestAIEnvironment(atoms=base_url, waves=base_url, waves_ws=base_url, payment=base_url),
httpx_client=httpx.Client(headers=test_headers),
api_key="test_token",
)
diff --git a/tests/wire/test_atoms_billing.py b/tests/wire/test_atoms_billing.py
new file mode 100644
index 00000000..11fe0e3d
--- /dev/null
+++ b/tests/wire/test_atoms_billing.py
@@ -0,0 +1,50 @@
+import datetime
+
+from .conftest import get_client, verify_request_count
+
+
+def test_atoms_billing_get_balance() -> None:
+ """Test get_balance endpoint with WireMock"""
+ test_id = "atoms.billing.get_balance.0"
+ client = get_client(test_id)
+ client.atoms.billing.get_balance()
+ verify_request_count(test_id, "GET", "/payment/v1/credits/balance", None, 1)
+
+
+def test_atoms_billing_get_ledger() -> None:
+ """Test get_ledger endpoint with WireMock"""
+ test_id = "atoms.billing.get_ledger.0"
+ client = get_client(test_id)
+ client.atoms.billing.get_ledger(
+ from_=datetime.datetime.fromisoformat("2026-07-01T00:00:00+00:00"),
+ to=datetime.datetime.fromisoformat("2026-07-28T00:00:00+00:00"),
+ )
+ verify_request_count(
+ test_id, "GET", "/payment/v1/credits/ledger", {"from": "2026-07-01T00:00:00Z", "to": "2026-07-28T00:00:00Z"}, 1
+ )
+
+
+def test_atoms_billing_get_usage_breakdown() -> None:
+ """Test get_usage_breakdown endpoint with WireMock"""
+ test_id = "atoms.billing.get_usage_breakdown.0"
+ client = get_client(test_id)
+ client.atoms.billing.get_usage_breakdown()
+ verify_request_count(test_id, "GET", "/payment/v1/credits/usage/breakdown", None, 1)
+
+
+def test_atoms_billing_list_invoices() -> None:
+ """Test list_invoices endpoint with WireMock"""
+ test_id = "atoms.billing.list_invoices.0"
+ client = get_client(test_id)
+ client.atoms.billing.list_invoices()
+ verify_request_count(test_id, "GET", "/payment/v1/invoices", None, 1)
+
+
+def test_atoms_billing_get_invoice_pdf() -> None:
+ """Test get_invoice_pdf endpoint with WireMock"""
+ test_id = "atoms.billing.get_invoice_pdf.0"
+ client = get_client(test_id)
+ client.atoms.billing.get_invoice_pdf(
+ invoice_id="invoiceId",
+ )
+ verify_request_count(test_id, "GET", "/payment/v1/invoices/invoiceId/pdf", None, 1)
diff --git a/wiremock/wiremock-mappings.json b/wiremock/wiremock-mappings.json
index abd0d9e2..97836e7e 100644
--- a/wiremock/wiremock-mappings.json
+++ b/wiremock/wiremock-mappings.json
@@ -4798,6 +4798,178 @@
},
"postServeActions": []
},
+ {
+ "id": "7a585eda-d514-4d17-b714-44d3f13490ae",
+ "name": "Get credit balance - default",
+ "request": {
+ "urlPathTemplate": "/payment/v1/credits/balance",
+ "method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
+ },
+ "response": {
+ "status": 200,
+ "body": "{\n \"success\": true,\n \"data\": {\n \"creditBalance\": 15,\n \"planId\": \"plan_standard\",\n \"isEnterprise\": false\n }\n}",
+ "headers": {
+ "Content-Type": "application/json"
+ }
+ },
+ "uuid": "7a585eda-d514-4d17-b714-44d3f13490ae",
+ "persistent": true,
+ "priority": 3,
+ "metadata": {
+ "mocklab": {
+ "created": {
+ "at": "2020-01-01T00:00:00.000Z",
+ "via": "SYSTEM"
+ }
+ }
+ },
+ "postServeActions": []
+ },
+ {
+ "id": "f17826c8-8354-4351-9daf-cf57d2fa4647",
+ "name": "List credit ledger - default",
+ "request": {
+ "urlPathTemplate": "/payment/v1/credits/ledger",
+ "method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
+ "queryParameters": {
+ "from": {
+ "equalTo": "2026-07-01T00:00:00Z"
+ },
+ "to": {
+ "equalTo": "2026-07-28T00:00:00Z"
+ }
+ }
+ },
+ "response": {
+ "status": 200,
+ "body": "{\n \"success\": true,\n \"data\": {\n \"transactions\": [\n {\n \"id\": \"id\",\n \"transactionType\": \"SIGNUP_BONUS\",\n \"amount\": 1.1,\n \"balanceAfter\": 1.1,\n \"createdAt\": \"2024-01-15T09:30:00Z\"\n }\n ],\n \"total\": 1,\n \"hasMore\": true,\n \"summary\": {\n \"totalCreditsConsumed\": 1.1,\n \"totalCreditsAdded\": 1.1\n },\n \"period\": {\n \"from\": \"2024-01-15T09:30:00Z\",\n \"to\": \"2024-01-15T09:30:00Z\"\n }\n }\n}",
+ "headers": {
+ "Content-Type": "application/json"
+ }
+ },
+ "uuid": "f17826c8-8354-4351-9daf-cf57d2fa4647",
+ "persistent": true,
+ "priority": 3,
+ "metadata": {
+ "mocklab": {
+ "created": {
+ "at": "2020-01-01T00:00:00.000Z",
+ "via": "SYSTEM"
+ }
+ }
+ },
+ "postServeActions": []
+ },
+ {
+ "id": "9c57aa7a-6bbc-4696-9d72-74558360bc33",
+ "name": "Get usage breakdown - default",
+ "request": {
+ "urlPathTemplate": "/payment/v1/credits/usage/breakdown",
+ "method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
+ },
+ "response": {
+ "status": 200,
+ "body": "{\n \"success\": true,\n \"data\": {\n \"platform\": 1.1,\n \"voiceAi\": 1.1,\n \"voiceModels\": 1.1\n }\n}",
+ "headers": {
+ "Content-Type": "application/json"
+ }
+ },
+ "uuid": "9c57aa7a-6bbc-4696-9d72-74558360bc33",
+ "persistent": true,
+ "priority": 3,
+ "metadata": {
+ "mocklab": {
+ "created": {
+ "at": "2020-01-01T00:00:00.000Z",
+ "via": "SYSTEM"
+ }
+ }
+ },
+ "postServeActions": []
+ },
+ {
+ "id": "658e481c-9633-4b82-877b-c6bb310a4218",
+ "name": "List invoices - default",
+ "request": {
+ "urlPathTemplate": "/payment/v1/invoices",
+ "method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
+ },
+ "response": {
+ "status": 200,
+ "body": "{\n \"success\": true,\n \"data\": [\n {\n \"key\": \"value\"\n }\n ]\n}",
+ "headers": {
+ "Content-Type": "application/json"
+ }
+ },
+ "uuid": "658e481c-9633-4b82-877b-c6bb310a4218",
+ "persistent": true,
+ "priority": 3,
+ "metadata": {
+ "mocklab": {
+ "created": {
+ "at": "2020-01-01T00:00:00.000Z",
+ "via": "SYSTEM"
+ }
+ }
+ },
+ "postServeActions": []
+ },
+ {
+ "id": "b67cb773-6ac8-4d0f-a178-05d1ed4984d6",
+ "name": "Get invoice PDF URL - default",
+ "request": {
+ "urlPathTemplate": "/payment/v1/invoices/{invoiceId}/pdf",
+ "method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
+ "pathParameters": {
+ "invoiceId": {
+ "equalTo": "invoiceId"
+ }
+ }
+ },
+ "response": {
+ "status": 200,
+ "body": "{\n \"success\": true,\n \"data\": {\n \"pdfUrl\": \"pdfUrl\"\n }\n}",
+ "headers": {
+ "Content-Type": "application/json"
+ }
+ },
+ "uuid": "b67cb773-6ac8-4d0f-a178-05d1ed4984d6",
+ "persistent": true,
+ "priority": 3,
+ "metadata": {
+ "mocklab": {
+ "created": {
+ "at": "2020-01-01T00:00:00.000Z",
+ "via": "SYSTEM"
+ }
+ }
+ }
+ },
{
"id": "23284c9e-d15e-4f3a-84b7-de9613ccf399",
"name": "Get pronunciation dictionaries - default",
@@ -5272,6 +5444,6 @@
}
],
"meta": {
- "total": 150
+ "total": 155
}
}
\ No newline at end of file
From a86206ff7b90089f47b1222c8e14e007564526c8 Mon Sep 17 00:00:00 2001
From: "fern-api[bot]" <115122769+fern-api[bot]@users.noreply.github.com>
Date: Tue, 28 Jul 2026 18:32:03 +0000
Subject: [PATCH 2/3] [fern-replay] Applied customizations
Patches applied (1):
- patch-0c9e9b4e: refactor: rename agent-swarm -> agent-crew across SDK + CLI (4.4.2)
---
.fern/replay.lock | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/.fern/replay.lock b/.fern/replay.lock
index ce1e26d2..0056a982 100644
--- a/.fern/replay.lock
+++ b/.fern/replay.lock
@@ -24,14 +24,20 @@ generations:
cli_version: unknown
generator_versions:
fernapi/fern-python-sdk: 5.12.12
-current_generation: 58539514affc27c6340e570ffd7f18d6c6806433
+ - commit_sha: 0a211d42874771b6c014819e8da42459bfdca96a
+ tree_hash: 8cb30599679840940039628b77c844caa2819777
+ timestamp: 2026-07-28T18:32:00.540Z
+ cli_version: unknown
+ generator_versions:
+ fernapi/fern-python-sdk: 5.12.12
+current_generation: 0a211d42874771b6c014819e8da42459bfdca96a
patches:
- id: patch-0c9e9b4e
content_hash: sha256:43cbb1c853a3de0a20a4d1e151169f4039eec0f4d998b32978cb5038f2a3b665
original_commit: 0c9e9b4e2286bd64653a1bdeb91a2b871a56668e
original_message: "refactor: rename agent-swarm -> agent-crew across SDK + CLI (4.4.2)"
original_author: Maharshi Chattopadhyay
- base_generation: 58539514affc27c6340e570ffd7f18d6c6806433
+ base_generation: 0a211d42874771b6c014819e8da42459bfdca96a
files:
- src/smallestai/atoms/swarm/__init__.py
- src/smallestai/atoms/swarm/clients/__init__.py
From 1891e3e6df3934e9e0d390aba5a293b7713754c0 Mon Sep 17 00:00:00 2001
From: Abhishek Mishra
Date: Wed, 29 Jul 2026 00:05:56 +0530
Subject: [PATCH 3/3] =?UTF-8?q?release:=205.3.1=20=E2=80=94=20billing=20en?=
=?UTF-8?q?dpoints,=20calls=20timestamp=20fix,=20agent-crew=20deploy=20Dev?=
=?UTF-8?q?X?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Bump 5.3.0 -> 5.3.1 + changelog.
- Billing rides the regen: client.atoms.billing (get_balance, get_ledger,
get_usage_breakdown, list_invoices, get_invoice_pdf) on the payment base.
- calls transcript timestamp now typed as string (ISO-8601), was number.
- agent-crew deploy DevX (cli/, .fernignore'd): pre-deploy layout check
(src/ layout, nested entry, pyproject-without-requirements) and a corrected
env-var warning pointing to the .env + load_dotenv path.
- Pass payment= to the four hand-written SmallestAIEnvironment constructions
(examples + velocity tests) now that the env has a payment url.
Gauntlet on the built wheel: mypy clean, scripts/verify.py PASSED, custom suite
41 passed, fresh-install imports + surface OK (atoms.billing present).
---
changelog.md | 14 ++++++++++
examples/build_voice_agent.py | 1 +
examples/quickstart.py | 1 +
pyproject.toml | 2 +-
src/smallestai/cli/agent_crew.py | 41 +++++++++++++++++++++++++----
tests/velocity/triage.py | 2 +-
tests/velocity/v2_versioning_e2e.py | 2 +-
7 files changed, 55 insertions(+), 8 deletions(-)
diff --git a/changelog.md b/changelog.md
index 26a301d2..60f63abc 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,17 @@
+## 5.3.1 - 2026-07-29
+
+Billing read endpoints, a call-log type fix, and clearer agent-crew deploy warnings. Additive; no breaking changes.
+
+* **Billing** (new `client.atoms.billing`): `get_balance`, `get_ledger`, `get_usage_breakdown`,
+ `list_invoices`, `get_invoice_pdf` (read-only credit and invoice endpoints under `/payment/v1`).
+* **Fix**: `calls.get` / `calls.list` transcript-turn `timestamp` is typed as a string (ISO-8601),
+ matching what the API returns. Previously typed as a number, which triggered a deserialization
+ warning.
+* **CLI DevX** (`smallestai agent-crew deploy`): a pre-deploy layout check warns on a `src/` layout,
+ a nested entry point, or a `pyproject.toml` with no `requirements.txt` (all fail the cloud build);
+ and the environment-variable warning now points to the working path (ship a `.env` at the project
+ root and call `load_dotenv()`; it deploys with your code) instead of just saying it is not propagated.
+
## 5.3.0 - 2026-07-28
Unified speech-to-text namespace (breaking), voice cloning, Electron LLM, webhook update.
diff --git a/examples/build_voice_agent.py b/examples/build_voice_agent.py
index fb9fbd4c..3e0b18ba 100644
--- a/examples/build_voice_agent.py
+++ b/examples/build_voice_agent.py
@@ -39,6 +39,7 @@ def make_client() -> SmallestAI:
atoms=f"{base}/atoms/v1",
waves=base,
waves_ws=base.replace("https", "wss").replace("http", "ws"),
+ payment=base,
)
return SmallestAI(api_key=api_key, environment=env)
return SmallestAI(api_key=api_key)
diff --git a/examples/quickstart.py b/examples/quickstart.py
index 5e1038bd..8dfceb04 100644
--- a/examples/quickstart.py
+++ b/examples/quickstart.py
@@ -25,6 +25,7 @@ def make_client() -> SmallestAI:
atoms=f"{base}/atoms/v1",
waves=base,
waves_ws=base.replace("https", "wss").replace("http", "ws"),
+ payment=base,
)
return SmallestAI(api_key=api_key, environment=env)
return SmallestAI(api_key=api_key) # production
diff --git a/pyproject.toml b/pyproject.toml
index 39a734b7..489e220e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ dynamic = ["version"]
[tool.poetry]
name = "smallestai"
-version = "5.3.0"
+version = "5.3.1"
description = ""
readme = "README.md"
authors = []
diff --git a/src/smallestai/cli/agent_crew.py b/src/smallestai/cli/agent_crew.py
index 84408ef9..4e44599c 100644
--- a/src/smallestai/cli/agent_crew.py
+++ b/src/smallestai/cli/agent_crew.py
@@ -175,6 +175,36 @@ async def async_deploy(directory: str, entry_point: str):
console.print(f"[dim]Entry point: {entry_point}[/dim]")
console.print(f"[dim]Agent ID: {agent_id}[/dim]\n")
+ # Pre-flight layout check. The cloud build runs `pip install .` from a
+ # pyproject before copying your source, so a src/ layout (or a nested
+ # entry point) fails there. The supported path is a flat directory with a
+ # requirements.txt (what the cookbook examples ship). Warn early instead
+ # of letting the build fail opaquely.
+ has_pyproject = (dir_path / "pyproject.toml").exists()
+ has_requirements = (dir_path / "requirements.txt").exists()
+ has_src_dir = (dir_path / "src").is_dir()
+ nested_entry = ("/" in entry_point) or ("\\" in entry_point)
+ if (has_pyproject and not has_requirements) or has_src_dir or nested_entry:
+ console.print("[yellow]⚠ Project layout may not build in the cloud:[/yellow]")
+ if has_src_dir or nested_entry:
+ console.print(
+ " [yellow]• src/ layout / nested entry point detected. The cloud "
+ "build expects a flat directory with the entry file at the root "
+ "(e.g. server.py), not under src/.[/yellow]"
+ )
+ if has_pyproject and not has_requirements:
+ console.print(
+ " [yellow]• pyproject.toml with no requirements.txt. The builder "
+ "runs `pip install .` before your source is copied, which fails for "
+ "src/ layouts. Add a requirements.txt (the builder prefers it and "
+ "skips that step).[/yellow]"
+ )
+ console.print(
+ "[dim]Supported layout: a flat directory with server.py + a "
+ "requirements.txt at the root. See the cookbook getting_started "
+ "example.[/dim]\n"
+ )
+
# Scan user code for env var references and warn — the deploy pipeline
# does not yet propagate `.env` values into the pod, so any env var
# the code reads must either be baked into the image or the call will
@@ -190,11 +220,12 @@ async def async_deploy(directory: str, entry_point: str):
for var in sorted(required_env_vars):
console.print(f" [yellow]• {var}[/yellow]")
console.print(
- "[dim]The deploy pipeline does not propagate `.env` values into the "
- "running pod yet. If these aren't set in the runtime image, your code "
- "will fail at first use (e.g. `openai.OpenAIError: Missing credentials`). "
- "Track: env-var propagation is the only outstanding blocker for many "
- "examples in cookbook.[/dim]\n"
+ "[dim]Platform-level env injection isn't available yet, but there is a "
+ "working path: put these in a `.env` file at your project root and call "
+ "`load_dotenv()` at startup (the cookbook examples already do). The `.env` "
+ "ships with your deploy and is loaded in the pod, so your code reads them "
+ "via `os.getenv(...)`. Without it, calls fail at first use "
+ "(e.g. `openai.OpenAIError: Missing credentials`).[/dim]\n"
)
# Create zip file in memory
diff --git a/tests/velocity/triage.py b/tests/velocity/triage.py
index 534b4aef..30f1d040 100644
--- a/tests/velocity/triage.py
+++ b/tests/velocity/triage.py
@@ -29,7 +29,7 @@ def agent_id_helper():
from smallestai import SmallestAI
from smallestai.environment import SmallestAIEnvironment
base = os.environ["SMALLEST_BASE_URL"].rstrip("/")
- env = SmallestAIEnvironment(atoms=f"{base}/atoms/v1", waves=base, waves_ws=base.replace("https", "wss"))
+ env = SmallestAIEnvironment(atoms=f"{base}/atoms/v1", waves=base, waves_ws=base.replace("https", "wss"), payment=base)
saved = {k: os.environ.pop(k, None) for k in ("SMALLEST_AI_TOKEN",)}
os.environ["SMALLEST_API_KEY"] = os.environ["SMALLEST_API_KEY"]
c2 = SmallestAI(environment=env) # no api_key arg -> must read SMALLEST_API_KEY
diff --git a/tests/velocity/v2_versioning_e2e.py b/tests/velocity/v2_versioning_e2e.py
index e7e5f5e0..24e4bb89 100644
--- a/tests/velocity/v2_versioning_e2e.py
+++ b/tests/velocity/v2_versioning_e2e.py
@@ -40,7 +40,7 @@ def make_client() -> SmallestAI:
if BASE:
host = BASE.split("/atoms")[0]
ws = host.replace("https://", "wss://").replace("http://", "ws://")
- env = SmallestAIEnvironment(atoms=BASE, waves=host, waves_ws=ws)
+ env = SmallestAIEnvironment(atoms=BASE, waves=host, waves_ws=ws, payment=host)
return SmallestAI(environment=env, api_key=KEY)
return SmallestAI(api_key=KEY)