From eed2900c69e717e346fe6d5d10d95be29771e233 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 30 Jan 2026 16:26:23 +0000
Subject: [PATCH 1/6] feat(api): api update
---
.stats.yml | 6 +-
api.md | 6 -
src/ark/resources/emails.py | 363 +-----------------
src/ark/resources/webhooks.py | 12 +
src/ark/types/__init__.py | 4 -
src/ark/types/email_list_response.py | 4 +-
.../email_retrieve_deliveries_response.py | 130 -------
src/ark/types/email_retrieve_params.py | 21 -
src/ark/types/email_retrieve_response.py | 184 ---------
src/ark/types/email_retry_response.py | 23 --
src/ark/types/email_send_batch_response.py | 4 +-
src/ark/types/email_send_raw_response.py | 2 +-
src/ark/types/email_send_response.py | 2 +-
src/ark/types/log_entry.py | 2 +-
src/ark/types/webhook_create_params.py | 2 +
src/ark/types/webhook_create_response.py | 2 +
.../types/webhook_list_deliveries_params.py | 2 +
.../types/webhook_list_deliveries_response.py | 2 +
.../webhook_retrieve_delivery_response.py | 2 +
src/ark/types/webhook_retrieve_response.py | 2 +
src/ark/types/webhook_test_params.py | 2 +
src/ark/types/webhook_update_response.py | 2 +
tests/api_resources/test_emails.py | 247 ------------
23 files changed, 41 insertions(+), 985 deletions(-)
delete mode 100644 src/ark/types/email_retrieve_deliveries_response.py
delete mode 100644 src/ark/types/email_retrieve_params.py
delete mode 100644 src/ark/types/email_retrieve_response.py
delete mode 100644 src/ark/types/email_retry_response.py
diff --git a/.stats.yml b/.stats.yml
index 69b13d9..631edab 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 35
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-39b91ffd46b6e41924f8465ffaaff6ba3c200a68daa513d4f1eb1e4b29aba78f.yml
-openapi_spec_hash: 542dd50007316698c83e8b0bdd5e40e2
+configured_endpoints: 32
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-65d92724dca2fcf00a43883ad10e0591b457891aa45ed965b0414b27388b5e16.yml
+openapi_spec_hash: 42824aaa51f95c0f485c91600969673f
config_hash: 77a3908ee910a8019f5831d3a3d53c18
diff --git a/api.md b/api.md
index 7baaad3..c296d0a 100644
--- a/api.md
+++ b/api.md
@@ -10,10 +10,7 @@ Types:
```python
from ark.types import (
- EmailRetrieveResponse,
EmailListResponse,
- EmailRetrieveDeliveriesResponse,
- EmailRetryResponse,
EmailSendResponse,
EmailSendBatchResponse,
EmailSendRawResponse,
@@ -22,10 +19,7 @@ from ark.types import (
Methods:
-- client.emails.retrieve(id, \*\*params) -> EmailRetrieveResponse
- client.emails.list(\*\*params) -> SyncPageNumberPagination[EmailListResponse]
-- client.emails.retrieve_deliveries(id) -> EmailRetrieveDeliveriesResponse
-- client.emails.retry(id) -> EmailRetryResponse
- client.emails.send(\*\*params) -> EmailSendResponse
- client.emails.send_batch(\*\*params) -> EmailSendBatchResponse
- client.emails.send_raw(\*\*params) -> EmailSendRawResponse
diff --git a/src/ark/resources/emails.py b/src/ark/resources/emails.py
index 4169d3a..d4eea62 100644
--- a/src/ark/resources/emails.py
+++ b/src/ark/resources/emails.py
@@ -7,13 +7,7 @@
import httpx
-from ..types import (
- email_list_params,
- email_send_params,
- email_retrieve_params,
- email_send_raw_params,
- email_send_batch_params,
-)
+from ..types import email_list_params, email_send_params, email_send_raw_params, email_send_batch_params
from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
from .._utils import maybe_transform, strip_not_given, async_maybe_transform
from .._compat import cached_property
@@ -28,11 +22,8 @@
from .._base_client import AsyncPaginator, make_request_options
from ..types.email_list_response import EmailListResponse
from ..types.email_send_response import EmailSendResponse
-from ..types.email_retry_response import EmailRetryResponse
-from ..types.email_retrieve_response import EmailRetrieveResponse
from ..types.email_send_raw_response import EmailSendRawResponse
from ..types.email_send_batch_response import EmailSendBatchResponse
-from ..types.email_retrieve_deliveries_response import EmailRetrieveDeliveriesResponse
__all__ = ["EmailsResource", "AsyncEmailsResource"]
@@ -57,59 +48,6 @@ def with_streaming_response(self) -> EmailsResourceWithStreamingResponse:
"""
return EmailsResourceWithStreamingResponse(self)
- def retrieve(
- self,
- id: str,
- *,
- expand: str | Omit = omit,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> EmailRetrieveResponse:
- """
- Retrieve detailed information about a specific email including delivery status,
- timestamps, and optionally the email content.
-
- Use the `expand` parameter to include additional data like the HTML/text body,
- headers, or delivery attempts.
-
- Args:
- expand:
- Comma-separated list of fields to include:
-
- - `full` - Include all expanded fields in a single request
- - `content` - HTML and plain text body
- - `headers` - Email headers
- - `deliveries` - Delivery attempt history
- - `activity` - Opens and clicks tracking data
- - `attachments` - File attachments with content (base64 encoded)
- - `raw` - Complete raw MIME message (base64 encoded)
-
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
- return self._get(
- f"/emails/{id}",
- options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=maybe_transform({"expand": expand}, email_retrieve_params.EmailRetrieveParams),
- ),
- cast_to=EmailRetrieveResponse,
- )
-
def list(
self,
*,
@@ -198,111 +136,6 @@ def list(
model=EmailListResponse,
)
- def retrieve_deliveries(
- self,
- id: str,
- *,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> EmailRetrieveDeliveriesResponse:
- """
- Get the complete delivery history for an email, including SMTP response codes,
- timestamps, and current retry state.
-
- ## Response Fields
-
- ### Status
-
- The current status of the email:
-
- - `pending` - Awaiting first delivery attempt
- - `sent` - Successfully delivered to recipient server
- - `softfail` - Temporary failure, automatic retry scheduled
- - `hardfail` - Permanent failure, will not retry
- - `held` - Held for manual review
- - `bounced` - Bounced by recipient server
-
- ### Retry State
-
- When the email is in the delivery queue (`pending` or `softfail` status),
- `retryState` provides information about the retry schedule:
-
- - `attempt` - Current attempt number (0 = first attempt)
- - `maxAttempts` - Maximum attempts before hard-fail (typically 18)
- - `attemptsRemaining` - Attempts left before hard-fail
- - `nextRetryAt` - When the next retry is scheduled (Unix timestamp)
- - `processing` - Whether the email is currently being processed
- - `manual` - Whether this was triggered by a manual retry
-
- When the email has finished processing (`sent`, `hardfail`, `held`, `bounced`),
- `retryState` is `null`.
-
- ### Can Retry Manually
-
- Indicates whether you can call `POST /emails/{id}/retry` to manually retry the
- email. This is `true` when the raw message content is still available (not
- expired due to retention policy).
-
- Args:
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
- return self._get(
- f"/emails/{id}/deliveries",
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=EmailRetrieveDeliveriesResponse,
- )
-
- def retry(
- self,
- id: str,
- *,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> EmailRetryResponse:
- """Retry delivery of a failed or soft-bounced email.
-
- Creates a new delivery
- attempt.
-
- Only works for emails that have failed or are in a retryable state.
-
- Args:
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
- return self._post(
- f"/emails/{id}/retry",
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=EmailRetryResponse,
- )
-
def send(
self,
*,
@@ -573,59 +406,6 @@ def with_streaming_response(self) -> AsyncEmailsResourceWithStreamingResponse:
"""
return AsyncEmailsResourceWithStreamingResponse(self)
- async def retrieve(
- self,
- id: str,
- *,
- expand: str | Omit = omit,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> EmailRetrieveResponse:
- """
- Retrieve detailed information about a specific email including delivery status,
- timestamps, and optionally the email content.
-
- Use the `expand` parameter to include additional data like the HTML/text body,
- headers, or delivery attempts.
-
- Args:
- expand:
- Comma-separated list of fields to include:
-
- - `full` - Include all expanded fields in a single request
- - `content` - HTML and plain text body
- - `headers` - Email headers
- - `deliveries` - Delivery attempt history
- - `activity` - Opens and clicks tracking data
- - `attachments` - File attachments with content (base64 encoded)
- - `raw` - Complete raw MIME message (base64 encoded)
-
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
- return await self._get(
- f"/emails/{id}",
- options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=await async_maybe_transform({"expand": expand}, email_retrieve_params.EmailRetrieveParams),
- ),
- cast_to=EmailRetrieveResponse,
- )
-
def list(
self,
*,
@@ -714,111 +494,6 @@ def list(
model=EmailListResponse,
)
- async def retrieve_deliveries(
- self,
- id: str,
- *,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> EmailRetrieveDeliveriesResponse:
- """
- Get the complete delivery history for an email, including SMTP response codes,
- timestamps, and current retry state.
-
- ## Response Fields
-
- ### Status
-
- The current status of the email:
-
- - `pending` - Awaiting first delivery attempt
- - `sent` - Successfully delivered to recipient server
- - `softfail` - Temporary failure, automatic retry scheduled
- - `hardfail` - Permanent failure, will not retry
- - `held` - Held for manual review
- - `bounced` - Bounced by recipient server
-
- ### Retry State
-
- When the email is in the delivery queue (`pending` or `softfail` status),
- `retryState` provides information about the retry schedule:
-
- - `attempt` - Current attempt number (0 = first attempt)
- - `maxAttempts` - Maximum attempts before hard-fail (typically 18)
- - `attemptsRemaining` - Attempts left before hard-fail
- - `nextRetryAt` - When the next retry is scheduled (Unix timestamp)
- - `processing` - Whether the email is currently being processed
- - `manual` - Whether this was triggered by a manual retry
-
- When the email has finished processing (`sent`, `hardfail`, `held`, `bounced`),
- `retryState` is `null`.
-
- ### Can Retry Manually
-
- Indicates whether you can call `POST /emails/{id}/retry` to manually retry the
- email. This is `true` when the raw message content is still available (not
- expired due to retention policy).
-
- Args:
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
- return await self._get(
- f"/emails/{id}/deliveries",
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=EmailRetrieveDeliveriesResponse,
- )
-
- async def retry(
- self,
- id: str,
- *,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> EmailRetryResponse:
- """Retry delivery of a failed or soft-bounced email.
-
- Creates a new delivery
- attempt.
-
- Only works for emails that have failed or are in a retryable state.
-
- Args:
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
- return await self._post(
- f"/emails/{id}/retry",
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=EmailRetryResponse,
- )
-
async def send(
self,
*,
@@ -1073,18 +748,9 @@ class EmailsResourceWithRawResponse:
def __init__(self, emails: EmailsResource) -> None:
self._emails = emails
- self.retrieve = to_raw_response_wrapper(
- emails.retrieve,
- )
self.list = to_raw_response_wrapper(
emails.list,
)
- self.retrieve_deliveries = to_raw_response_wrapper(
- emails.retrieve_deliveries,
- )
- self.retry = to_raw_response_wrapper(
- emails.retry,
- )
self.send = to_raw_response_wrapper(
emails.send,
)
@@ -1100,18 +766,9 @@ class AsyncEmailsResourceWithRawResponse:
def __init__(self, emails: AsyncEmailsResource) -> None:
self._emails = emails
- self.retrieve = async_to_raw_response_wrapper(
- emails.retrieve,
- )
self.list = async_to_raw_response_wrapper(
emails.list,
)
- self.retrieve_deliveries = async_to_raw_response_wrapper(
- emails.retrieve_deliveries,
- )
- self.retry = async_to_raw_response_wrapper(
- emails.retry,
- )
self.send = async_to_raw_response_wrapper(
emails.send,
)
@@ -1127,18 +784,9 @@ class EmailsResourceWithStreamingResponse:
def __init__(self, emails: EmailsResource) -> None:
self._emails = emails
- self.retrieve = to_streamed_response_wrapper(
- emails.retrieve,
- )
self.list = to_streamed_response_wrapper(
emails.list,
)
- self.retrieve_deliveries = to_streamed_response_wrapper(
- emails.retrieve_deliveries,
- )
- self.retry = to_streamed_response_wrapper(
- emails.retry,
- )
self.send = to_streamed_response_wrapper(
emails.send,
)
@@ -1154,18 +802,9 @@ class AsyncEmailsResourceWithStreamingResponse:
def __init__(self, emails: AsyncEmailsResource) -> None:
self._emails = emails
- self.retrieve = async_to_streamed_response_wrapper(
- emails.retrieve,
- )
self.list = async_to_streamed_response_wrapper(
emails.list,
)
- self.retrieve_deliveries = async_to_streamed_response_wrapper(
- emails.retrieve_deliveries,
- )
- self.retry = async_to_streamed_response_wrapper(
- emails.retry,
- )
self.send = async_to_streamed_response_wrapper(
emails.send,
)
diff --git a/src/ark/resources/webhooks.py b/src/ark/resources/webhooks.py
index 97b0202..f59179e 100644
--- a/src/ark/resources/webhooks.py
+++ b/src/ark/resources/webhooks.py
@@ -70,6 +70,8 @@ def create(
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
+ "SendLimitApproaching",
+ "SendLimitExceeded",
]
]
]
@@ -290,6 +292,8 @@ def list_deliveries(
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
+ "SendLimitApproaching",
+ "SendLimitExceeded",
]
| Omit = omit,
page: int | Omit = omit,
@@ -474,6 +478,8 @@ def test(
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
+ "SendLimitApproaching",
+ "SendLimitExceeded",
],
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -557,6 +563,8 @@ async def create(
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
+ "SendLimitApproaching",
+ "SendLimitExceeded",
]
]
]
@@ -777,6 +785,8 @@ async def list_deliveries(
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
+ "SendLimitApproaching",
+ "SendLimitExceeded",
]
| Omit = omit,
page: int | Omit = omit,
@@ -961,6 +971,8 @@ async def test(
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
+ "SendLimitApproaching",
+ "SendLimitExceeded",
],
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
diff --git a/src/ark/types/__init__.py b/src/ark/types/__init__.py
index 6cd75fb..5c42442 100644
--- a/src/ark/types/__init__.py
+++ b/src/ark/types/__init__.py
@@ -15,8 +15,6 @@
from .webhook_test_params import WebhookTestParams as WebhookTestParams
from .domain_create_params import DomainCreateParams as DomainCreateParams
from .domain_list_response import DomainListResponse as DomainListResponse
-from .email_retry_response import EmailRetryResponse as EmailRetryResponse
-from .email_retrieve_params import EmailRetrieveParams as EmailRetrieveParams
from .email_send_raw_params import EmailSendRawParams as EmailSendRawParams
from .log_retrieve_response import LogRetrieveResponse as LogRetrieveResponse
from .webhook_create_params import WebhookCreateParams as WebhookCreateParams
@@ -29,7 +27,6 @@
from .tracking_create_params import TrackingCreateParams as TrackingCreateParams
from .tracking_list_response import TrackingListResponse as TrackingListResponse
from .tracking_update_params import TrackingUpdateParams as TrackingUpdateParams
-from .email_retrieve_response import EmailRetrieveResponse as EmailRetrieveResponse
from .email_send_batch_params import EmailSendBatchParams as EmailSendBatchParams
from .email_send_raw_response import EmailSendRawResponse as EmailSendRawResponse
from .suppression_list_params import SuppressionListParams as SuppressionListParams
@@ -55,5 +52,4 @@
from .suppression_bulk_create_response import SuppressionBulkCreateResponse as SuppressionBulkCreateResponse
from .webhook_list_deliveries_response import WebhookListDeliveriesResponse as WebhookListDeliveriesResponse
from .webhook_replay_delivery_response import WebhookReplayDeliveryResponse as WebhookReplayDeliveryResponse
-from .email_retrieve_deliveries_response import EmailRetrieveDeliveriesResponse as EmailRetrieveDeliveriesResponse
from .webhook_retrieve_delivery_response import WebhookRetrieveDeliveryResponse as WebhookRetrieveDeliveryResponse
diff --git a/src/ark/types/email_list_response.py b/src/ark/types/email_list_response.py
index dce7d39..f8a1d1c 100644
--- a/src/ark/types/email_list_response.py
+++ b/src/ark/types/email_list_response.py
@@ -13,7 +13,9 @@
class EmailListResponse(BaseModel):
id: str
- """Unique message identifier (token)"""
+ """Internal message ID"""
+
+ token: str
from_: str = FieldInfo(alias="from")
diff --git a/src/ark/types/email_retrieve_deliveries_response.py b/src/ark/types/email_retrieve_deliveries_response.py
deleted file mode 100644
index 6406e9e..0000000
--- a/src/ark/types/email_retrieve_deliveries_response.py
+++ /dev/null
@@ -1,130 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing import List, Optional
-from datetime import datetime
-from typing_extensions import Literal
-
-from pydantic import Field as FieldInfo
-
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
-
-__all__ = ["EmailRetrieveDeliveriesResponse", "Data", "DataDelivery", "DataRetryState"]
-
-
-class DataDelivery(BaseModel):
- id: str
- """Delivery attempt ID"""
-
- status: str
- """Delivery status (lowercase)"""
-
- timestamp: float
- """Unix timestamp"""
-
- timestamp_iso: datetime = FieldInfo(alias="timestampIso")
- """ISO 8601 timestamp"""
-
- code: Optional[int] = None
- """SMTP response code"""
-
- details: Optional[str] = None
- """Status details"""
-
- output: Optional[str] = None
- """SMTP server response from the receiving mail server"""
-
- sent_with_ssl: Optional[bool] = FieldInfo(alias="sentWithSsl", default=None)
- """Whether TLS was used"""
-
-
-class DataRetryState(BaseModel):
- """
- Information about the current retry state of a message that is queued for delivery.
- Only present when the message is in the delivery queue.
- """
-
- attempt: int
- """Current attempt number (0-indexed).
-
- The first delivery attempt is 0, the first retry is 1, and so on.
- """
-
- attempts_remaining: int = FieldInfo(alias="attemptsRemaining")
- """
- Number of attempts remaining before the message is hard-failed. Calculated as
- `maxAttempts - attempt`.
- """
-
- manual: bool
- """
- Whether this queue entry was created by a manual retry request. Manual retries
- bypass certain hold conditions like suppression lists.
- """
-
- max_attempts: int = FieldInfo(alias="maxAttempts")
- """
- Maximum number of delivery attempts before the message is hard-failed.
- Configured at the server level.
- """
-
- processing: bool
- """
- Whether the message is currently being processed by a delivery worker. When
- `true`, the message is actively being sent.
- """
-
- next_retry_at: Optional[float] = FieldInfo(alias="nextRetryAt", default=None)
- """
- Unix timestamp of when the next retry attempt is scheduled. `null` if the
- message is ready for immediate processing or currently being processed.
- """
-
- next_retry_at_iso: Optional[datetime] = FieldInfo(alias="nextRetryAtIso", default=None)
- """
- ISO 8601 formatted timestamp of the next retry attempt. `null` if the message is
- ready for immediate processing.
- """
-
-
-class Data(BaseModel):
- id: str
- """Message identifier (token)"""
-
- can_retry_manually: bool = FieldInfo(alias="canRetryManually")
- """
- Whether the message can be manually retried via `POST /emails/{id}/retry`.
- `true` when the raw message content is still available (not expired). Messages
- older than the retention period cannot be retried.
- """
-
- deliveries: List[DataDelivery]
- """
- Chronological list of delivery attempts for this message. Each attempt includes
- SMTP response codes and timestamps.
- """
-
- retry_state: Optional[DataRetryState] = FieldInfo(alias="retryState", default=None)
- """
- Information about the current retry state of a message that is queued for
- delivery. Only present when the message is in the delivery queue.
- """
-
- status: Literal["pending", "sent", "softfail", "hardfail", "held", "bounced"]
- """Current message status (lowercase). Possible values:
-
- - `pending` - Initial state, awaiting first delivery attempt
- - `sent` - Successfully delivered
- - `softfail` - Temporary failure, will retry automatically
- - `hardfail` - Permanent failure, will not retry
- - `held` - Held for manual review (suppression list, etc.)
- - `bounced` - Bounced by recipient server
- """
-
-
-class EmailRetrieveDeliveriesResponse(BaseModel):
- data: Data
-
- meta: APIMeta
-
- success: Literal[True]
diff --git a/src/ark/types/email_retrieve_params.py b/src/ark/types/email_retrieve_params.py
deleted file mode 100644
index 5d3fc43..0000000
--- a/src/ark/types/email_retrieve_params.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-from typing_extensions import TypedDict
-
-__all__ = ["EmailRetrieveParams"]
-
-
-class EmailRetrieveParams(TypedDict, total=False):
- expand: str
- """Comma-separated list of fields to include:
-
- - `full` - Include all expanded fields in a single request
- - `content` - HTML and plain text body
- - `headers` - Email headers
- - `deliveries` - Delivery attempt history
- - `activity` - Opens and clicks tracking data
- - `attachments` - File attachments with content (base64 encoded)
- - `raw` - Complete raw MIME message (base64 encoded)
- """
diff --git a/src/ark/types/email_retrieve_response.py b/src/ark/types/email_retrieve_response.py
deleted file mode 100644
index f60801e..0000000
--- a/src/ark/types/email_retrieve_response.py
+++ /dev/null
@@ -1,184 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing import Dict, List, Optional
-from datetime import datetime
-from typing_extensions import Literal
-
-from pydantic import Field as FieldInfo
-
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
-
-__all__ = [
- "EmailRetrieveResponse",
- "Data",
- "DataActivity",
- "DataActivityClick",
- "DataActivityOpen",
- "DataAttachment",
- "DataDelivery",
-]
-
-
-class DataActivityClick(BaseModel):
- ip_address: Optional[str] = FieldInfo(alias="ipAddress", default=None)
- """IP address of the clicker"""
-
- timestamp: Optional[float] = None
- """Unix timestamp of the click event"""
-
- timestamp_iso: Optional[datetime] = FieldInfo(alias="timestampIso", default=None)
- """ISO 8601 timestamp of the click event"""
-
- url: Optional[str] = None
- """URL that was clicked"""
-
- user_agent: Optional[str] = FieldInfo(alias="userAgent", default=None)
- """User agent of the email client"""
-
-
-class DataActivityOpen(BaseModel):
- ip_address: Optional[str] = FieldInfo(alias="ipAddress", default=None)
- """IP address of the opener"""
-
- timestamp: Optional[float] = None
- """Unix timestamp of the open event"""
-
- timestamp_iso: Optional[datetime] = FieldInfo(alias="timestampIso", default=None)
- """ISO 8601 timestamp of the open event"""
-
- user_agent: Optional[str] = FieldInfo(alias="userAgent", default=None)
- """User agent of the email client"""
-
-
-class DataActivity(BaseModel):
- """Opens and clicks tracking data (included if expand=activity)"""
-
- clicks: Optional[List[DataActivityClick]] = None
- """List of link click events"""
-
- opens: Optional[List[DataActivityOpen]] = None
- """List of email open events"""
-
-
-class DataAttachment(BaseModel):
- """An email attachment retrieved from a sent message"""
-
- content_type: str = FieldInfo(alias="contentType")
- """MIME type of the attachment"""
-
- data: str
- """Base64 encoded attachment content. Decode this to get the raw file bytes."""
-
- filename: str
- """Original filename of the attachment"""
-
- hash: str
- """SHA256 hash of the attachment content for verification"""
-
- size: int
- """Size of the attachment in bytes"""
-
-
-class DataDelivery(BaseModel):
- id: str
- """Delivery attempt ID"""
-
- status: str
- """Delivery status (lowercase)"""
-
- timestamp: float
- """Unix timestamp"""
-
- timestamp_iso: datetime = FieldInfo(alias="timestampIso")
- """ISO 8601 timestamp"""
-
- code: Optional[int] = None
- """SMTP response code"""
-
- details: Optional[str] = None
- """Status details"""
-
- output: Optional[str] = None
- """SMTP server response from the receiving mail server"""
-
- sent_with_ssl: Optional[bool] = FieldInfo(alias="sentWithSsl", default=None)
- """Whether TLS was used"""
-
-
-class Data(BaseModel):
- id: str
- """Unique message identifier (token)"""
-
- from_: str = FieldInfo(alias="from")
- """Sender address"""
-
- scope: Literal["outgoing", "incoming"]
- """Message direction"""
-
- status: Literal["pending", "sent", "softfail", "hardfail", "bounced", "held"]
- """Current delivery status:
-
- - `pending` - Email accepted, waiting to be processed
- - `sent` - Email transmitted to recipient's mail server
- - `softfail` - Temporary delivery failure, will retry
- - `hardfail` - Permanent delivery failure
- - `bounced` - Email bounced back
- - `held` - Held for manual review
- """
-
- subject: str
- """Email subject line"""
-
- timestamp: float
- """Unix timestamp when the email was sent"""
-
- timestamp_iso: datetime = FieldInfo(alias="timestampIso")
- """ISO 8601 formatted timestamp"""
-
- to: str
- """Recipient address"""
-
- activity: Optional[DataActivity] = None
- """Opens and clicks tracking data (included if expand=activity)"""
-
- attachments: Optional[List[DataAttachment]] = None
- """File attachments (included if expand=attachments)"""
-
- deliveries: Optional[List[DataDelivery]] = None
- """Delivery attempt history (included if expand=deliveries)"""
-
- headers: Optional[Dict[str, str]] = None
- """Email headers (included if expand=headers)"""
-
- html_body: Optional[str] = FieldInfo(alias="htmlBody", default=None)
- """HTML body content (included if expand=content)"""
-
- message_id: Optional[str] = FieldInfo(alias="messageId", default=None)
- """SMTP Message-ID header"""
-
- plain_body: Optional[str] = FieldInfo(alias="plainBody", default=None)
- """Plain text body (included if expand=content)"""
-
- raw_message: Optional[str] = FieldInfo(alias="rawMessage", default=None)
- """
- Complete raw MIME message, base64 encoded (included if expand=raw). Decode this
- to get the original RFC 2822 formatted email.
- """
-
- spam: Optional[bool] = None
- """Whether the message was flagged as spam"""
-
- spam_score: Optional[float] = FieldInfo(alias="spamScore", default=None)
- """Spam score (if applicable)"""
-
- tag: Optional[str] = None
- """Optional categorization tag"""
-
-
-class EmailRetrieveResponse(BaseModel):
- data: Data
-
- meta: APIMeta
-
- success: Literal[True]
diff --git a/src/ark/types/email_retry_response.py b/src/ark/types/email_retry_response.py
deleted file mode 100644
index f63cc99..0000000
--- a/src/ark/types/email_retry_response.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing_extensions import Literal
-
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
-
-__all__ = ["EmailRetryResponse", "Data"]
-
-
-class Data(BaseModel):
- id: str
- """Email identifier (token)"""
-
- message: str
-
-
-class EmailRetryResponse(BaseModel):
- data: Data
-
- meta: APIMeta
-
- success: Literal[True]
diff --git a/src/ark/types/email_send_batch_response.py b/src/ark/types/email_send_batch_response.py
index d32e3fc..272119c 100644
--- a/src/ark/types/email_send_batch_response.py
+++ b/src/ark/types/email_send_batch_response.py
@@ -11,7 +11,9 @@
class DataMessages(BaseModel):
id: str
- """Message identifier (token)"""
+ """Message ID"""
+
+ token: str
class Data(BaseModel):
diff --git a/src/ark/types/email_send_raw_response.py b/src/ark/types/email_send_raw_response.py
index 46e7c96..f1e8c03 100644
--- a/src/ark/types/email_send_raw_response.py
+++ b/src/ark/types/email_send_raw_response.py
@@ -13,7 +13,7 @@
class Data(BaseModel):
id: str
- """Unique message identifier (token)"""
+ """Unique message ID (format: msg*{id}*{token})"""
status: Literal["pending", "sent"]
"""Current delivery status"""
diff --git a/src/ark/types/email_send_response.py b/src/ark/types/email_send_response.py
index cb7d814..39e1d36 100644
--- a/src/ark/types/email_send_response.py
+++ b/src/ark/types/email_send_response.py
@@ -13,7 +13,7 @@
class Data(BaseModel):
id: str
- """Unique message identifier (token)"""
+ """Unique message ID (format: msg*{id}*{token})"""
status: Literal["pending", "sent"]
"""Current delivery status"""
diff --git a/src/ark/types/log_entry.py b/src/ark/types/log_entry.py
index 4dfdd6c..9fad7e7 100644
--- a/src/ark/types/log_entry.py
+++ b/src/ark/types/log_entry.py
@@ -57,7 +57,7 @@ class Email(BaseModel):
"""Email-specific data (for email endpoints)"""
id: Optional[str] = None
- """Email message identifier (token)"""
+ """Email message ID"""
recipient_count: Optional[int] = FieldInfo(alias="recipientCount", default=None)
"""Number of recipients"""
diff --git a/src/ark/types/webhook_create_params.py b/src/ark/types/webhook_create_params.py
index 9899826..8b7a63e 100644
--- a/src/ark/types/webhook_create_params.py
+++ b/src/ark/types/webhook_create_params.py
@@ -34,6 +34,8 @@ class WebhookCreateParams(TypedDict, total=False):
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
+ "SendLimitApproaching",
+ "SendLimitExceeded",
]
]
]
diff --git a/src/ark/types/webhook_create_response.py b/src/ark/types/webhook_create_response.py
index d78bb58..07f75ec 100644
--- a/src/ark/types/webhook_create_response.py
+++ b/src/ark/types/webhook_create_response.py
@@ -34,6 +34,8 @@ class Data(BaseModel):
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
+ "SendLimitApproaching",
+ "SendLimitExceeded",
]
]
"""Subscribed events"""
diff --git a/src/ark/types/webhook_list_deliveries_params.py b/src/ark/types/webhook_list_deliveries_params.py
index c4048e4..cce2c61 100644
--- a/src/ark/types/webhook_list_deliveries_params.py
+++ b/src/ark/types/webhook_list_deliveries_params.py
@@ -25,6 +25,8 @@ class WebhookListDeliveriesParams(TypedDict, total=False):
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
+ "SendLimitApproaching",
+ "SendLimitExceeded",
]
"""Filter by event type"""
diff --git a/src/ark/types/webhook_list_deliveries_response.py b/src/ark/types/webhook_list_deliveries_response.py
index 71a5fe2..cb5033d 100644
--- a/src/ark/types/webhook_list_deliveries_response.py
+++ b/src/ark/types/webhook_list_deliveries_response.py
@@ -30,6 +30,8 @@ class Data(BaseModel):
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
+ "SendLimitApproaching",
+ "SendLimitExceeded",
]
"""Event type that triggered this delivery"""
diff --git a/src/ark/types/webhook_retrieve_delivery_response.py b/src/ark/types/webhook_retrieve_delivery_response.py
index efec5c4..9a0c38b 100644
--- a/src/ark/types/webhook_retrieve_delivery_response.py
+++ b/src/ark/types/webhook_retrieve_delivery_response.py
@@ -50,6 +50,8 @@ class Data(BaseModel):
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
+ "SendLimitApproaching",
+ "SendLimitExceeded",
]
"""Event type that triggered this delivery"""
diff --git a/src/ark/types/webhook_retrieve_response.py b/src/ark/types/webhook_retrieve_response.py
index 6cb2f2e..db780ca 100644
--- a/src/ark/types/webhook_retrieve_response.py
+++ b/src/ark/types/webhook_retrieve_response.py
@@ -34,6 +34,8 @@ class Data(BaseModel):
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
+ "SendLimitApproaching",
+ "SendLimitExceeded",
]
]
"""Subscribed events"""
diff --git a/src/ark/types/webhook_test_params.py b/src/ark/types/webhook_test_params.py
index 3f91fca..5648540 100644
--- a/src/ark/types/webhook_test_params.py
+++ b/src/ark/types/webhook_test_params.py
@@ -18,6 +18,8 @@ class WebhookTestParams(TypedDict, total=False):
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
+ "SendLimitApproaching",
+ "SendLimitExceeded",
]
]
"""Event type to simulate"""
diff --git a/src/ark/types/webhook_update_response.py b/src/ark/types/webhook_update_response.py
index 4d9a0c6..75c6d59 100644
--- a/src/ark/types/webhook_update_response.py
+++ b/src/ark/types/webhook_update_response.py
@@ -34,6 +34,8 @@ class Data(BaseModel):
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
+ "SendLimitApproaching",
+ "SendLimitExceeded",
]
]
"""Subscribed events"""
diff --git a/tests/api_resources/test_emails.py b/tests/api_resources/test_emails.py
index 7a54df7..e416353 100644
--- a/tests/api_resources/test_emails.py
+++ b/tests/api_resources/test_emails.py
@@ -11,11 +11,8 @@
from ark.types import (
EmailListResponse,
EmailSendResponse,
- EmailRetryResponse,
EmailSendRawResponse,
- EmailRetrieveResponse,
EmailSendBatchResponse,
- EmailRetrieveDeliveriesResponse,
)
from tests.utils import assert_matches_type
from ark.pagination import SyncPageNumberPagination, AsyncPageNumberPagination
@@ -26,52 +23,6 @@
class TestEmails:
parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
- @parametrize
- def test_method_retrieve(self, client: Ark) -> None:
- email = client.emails.retrieve(
- id="aBc123XyZ",
- )
- assert_matches_type(EmailRetrieveResponse, email, path=["response"])
-
- @parametrize
- def test_method_retrieve_with_all_params(self, client: Ark) -> None:
- email = client.emails.retrieve(
- id="aBc123XyZ",
- expand="full",
- )
- assert_matches_type(EmailRetrieveResponse, email, path=["response"])
-
- @parametrize
- def test_raw_response_retrieve(self, client: Ark) -> None:
- response = client.emails.with_raw_response.retrieve(
- id="aBc123XyZ",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- email = response.parse()
- assert_matches_type(EmailRetrieveResponse, email, path=["response"])
-
- @parametrize
- def test_streaming_response_retrieve(self, client: Ark) -> None:
- with client.emails.with_streaming_response.retrieve(
- id="aBc123XyZ",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- email = response.parse()
- assert_matches_type(EmailRetrieveResponse, email, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @parametrize
- def test_path_params_retrieve(self, client: Ark) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
- client.emails.with_raw_response.retrieve(
- id="",
- )
-
@parametrize
def test_method_list(self, client: Ark) -> None:
email = client.emails.list()
@@ -111,82 +62,6 @@ def test_streaming_response_list(self, client: Ark) -> None:
assert cast(Any, response.is_closed) is True
- @parametrize
- def test_method_retrieve_deliveries(self, client: Ark) -> None:
- email = client.emails.retrieve_deliveries(
- "aBc123XyZ",
- )
- assert_matches_type(EmailRetrieveDeliveriesResponse, email, path=["response"])
-
- @parametrize
- def test_raw_response_retrieve_deliveries(self, client: Ark) -> None:
- response = client.emails.with_raw_response.retrieve_deliveries(
- "aBc123XyZ",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- email = response.parse()
- assert_matches_type(EmailRetrieveDeliveriesResponse, email, path=["response"])
-
- @parametrize
- def test_streaming_response_retrieve_deliveries(self, client: Ark) -> None:
- with client.emails.with_streaming_response.retrieve_deliveries(
- "aBc123XyZ",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- email = response.parse()
- assert_matches_type(EmailRetrieveDeliveriesResponse, email, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @parametrize
- def test_path_params_retrieve_deliveries(self, client: Ark) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
- client.emails.with_raw_response.retrieve_deliveries(
- "",
- )
-
- @parametrize
- def test_method_retry(self, client: Ark) -> None:
- email = client.emails.retry(
- "aBc123XyZ",
- )
- assert_matches_type(EmailRetryResponse, email, path=["response"])
-
- @parametrize
- def test_raw_response_retry(self, client: Ark) -> None:
- response = client.emails.with_raw_response.retry(
- "aBc123XyZ",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- email = response.parse()
- assert_matches_type(EmailRetryResponse, email, path=["response"])
-
- @parametrize
- def test_streaming_response_retry(self, client: Ark) -> None:
- with client.emails.with_streaming_response.retry(
- "aBc123XyZ",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- email = response.parse()
- assert_matches_type(EmailRetryResponse, email, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @parametrize
- def test_path_params_retry(self, client: Ark) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
- client.emails.with_raw_response.retry(
- "",
- )
-
@parametrize
def test_method_send(self, client: Ark) -> None:
email = client.emails.send(
@@ -400,52 +275,6 @@ class TestAsyncEmails:
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
)
- @parametrize
- async def test_method_retrieve(self, async_client: AsyncArk) -> None:
- email = await async_client.emails.retrieve(
- id="aBc123XyZ",
- )
- assert_matches_type(EmailRetrieveResponse, email, path=["response"])
-
- @parametrize
- async def test_method_retrieve_with_all_params(self, async_client: AsyncArk) -> None:
- email = await async_client.emails.retrieve(
- id="aBc123XyZ",
- expand="full",
- )
- assert_matches_type(EmailRetrieveResponse, email, path=["response"])
-
- @parametrize
- async def test_raw_response_retrieve(self, async_client: AsyncArk) -> None:
- response = await async_client.emails.with_raw_response.retrieve(
- id="aBc123XyZ",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- email = await response.parse()
- assert_matches_type(EmailRetrieveResponse, email, path=["response"])
-
- @parametrize
- async def test_streaming_response_retrieve(self, async_client: AsyncArk) -> None:
- async with async_client.emails.with_streaming_response.retrieve(
- id="aBc123XyZ",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- email = await response.parse()
- assert_matches_type(EmailRetrieveResponse, email, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @parametrize
- async def test_path_params_retrieve(self, async_client: AsyncArk) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
- await async_client.emails.with_raw_response.retrieve(
- id="",
- )
-
@parametrize
async def test_method_list(self, async_client: AsyncArk) -> None:
email = await async_client.emails.list()
@@ -485,82 +314,6 @@ async def test_streaming_response_list(self, async_client: AsyncArk) -> None:
assert cast(Any, response.is_closed) is True
- @parametrize
- async def test_method_retrieve_deliveries(self, async_client: AsyncArk) -> None:
- email = await async_client.emails.retrieve_deliveries(
- "aBc123XyZ",
- )
- assert_matches_type(EmailRetrieveDeliveriesResponse, email, path=["response"])
-
- @parametrize
- async def test_raw_response_retrieve_deliveries(self, async_client: AsyncArk) -> None:
- response = await async_client.emails.with_raw_response.retrieve_deliveries(
- "aBc123XyZ",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- email = await response.parse()
- assert_matches_type(EmailRetrieveDeliveriesResponse, email, path=["response"])
-
- @parametrize
- async def test_streaming_response_retrieve_deliveries(self, async_client: AsyncArk) -> None:
- async with async_client.emails.with_streaming_response.retrieve_deliveries(
- "aBc123XyZ",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- email = await response.parse()
- assert_matches_type(EmailRetrieveDeliveriesResponse, email, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @parametrize
- async def test_path_params_retrieve_deliveries(self, async_client: AsyncArk) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
- await async_client.emails.with_raw_response.retrieve_deliveries(
- "",
- )
-
- @parametrize
- async def test_method_retry(self, async_client: AsyncArk) -> None:
- email = await async_client.emails.retry(
- "aBc123XyZ",
- )
- assert_matches_type(EmailRetryResponse, email, path=["response"])
-
- @parametrize
- async def test_raw_response_retry(self, async_client: AsyncArk) -> None:
- response = await async_client.emails.with_raw_response.retry(
- "aBc123XyZ",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- email = await response.parse()
- assert_matches_type(EmailRetryResponse, email, path=["response"])
-
- @parametrize
- async def test_streaming_response_retry(self, async_client: AsyncArk) -> None:
- async with async_client.emails.with_streaming_response.retry(
- "aBc123XyZ",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- email = await response.parse()
- assert_matches_type(EmailRetryResponse, email, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @parametrize
- async def test_path_params_retry(self, async_client: AsyncArk) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
- await async_client.emails.with_raw_response.retry(
- "",
- )
-
@parametrize
async def test_method_send(self, async_client: AsyncArk) -> None:
email = await async_client.emails.send(
From e310cbdd0ce739190f1094b6ea1c5dd539eb91c4 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 2 Feb 2026 21:25:37 +0000
Subject: [PATCH 2/6] feat(api): manual updates
---
.stats.yml | 6 +-
api.md | 6 +
src/ark/resources/emails.py | 363 +++++++++++++++++-
src/ark/resources/webhooks.py | 12 -
src/ark/types/__init__.py | 4 +
src/ark/types/email_list_response.py | 4 +-
.../email_retrieve_deliveries_response.py | 130 +++++++
src/ark/types/email_retrieve_params.py | 21 +
src/ark/types/email_retrieve_response.py | 184 +++++++++
src/ark/types/email_retry_response.py | 23 ++
src/ark/types/email_send_batch_response.py | 4 +-
src/ark/types/email_send_raw_response.py | 2 +-
src/ark/types/email_send_response.py | 2 +-
src/ark/types/log_entry.py | 2 +-
src/ark/types/webhook_create_params.py | 2 -
src/ark/types/webhook_create_response.py | 2 -
.../types/webhook_list_deliveries_params.py | 2 -
.../types/webhook_list_deliveries_response.py | 2 -
.../webhook_retrieve_delivery_response.py | 2 -
src/ark/types/webhook_retrieve_response.py | 2 -
src/ark/types/webhook_test_params.py | 2 -
src/ark/types/webhook_update_response.py | 2 -
tests/api_resources/test_emails.py | 247 ++++++++++++
23 files changed, 985 insertions(+), 41 deletions(-)
create mode 100644 src/ark/types/email_retrieve_deliveries_response.py
create mode 100644 src/ark/types/email_retrieve_params.py
create mode 100644 src/ark/types/email_retrieve_response.py
create mode 100644 src/ark/types/email_retry_response.py
diff --git a/.stats.yml b/.stats.yml
index 631edab..17e53d5 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 32
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-65d92724dca2fcf00a43883ad10e0591b457891aa45ed965b0414b27388b5e16.yml
-openapi_spec_hash: 42824aaa51f95c0f485c91600969673f
+configured_endpoints: 35
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-13bda63d06cee34ce339a861539358c6322e3c3964b2ef9994621de91138ef35.yml
+openapi_spec_hash: db21296328b817af863e09c31596316f
config_hash: 77a3908ee910a8019f5831d3a3d53c18
diff --git a/api.md b/api.md
index c296d0a..7baaad3 100644
--- a/api.md
+++ b/api.md
@@ -10,7 +10,10 @@ Types:
```python
from ark.types import (
+ EmailRetrieveResponse,
EmailListResponse,
+ EmailRetrieveDeliveriesResponse,
+ EmailRetryResponse,
EmailSendResponse,
EmailSendBatchResponse,
EmailSendRawResponse,
@@ -19,7 +22,10 @@ from ark.types import (
Methods:
+- client.emails.retrieve(id, \*\*params) -> EmailRetrieveResponse
- client.emails.list(\*\*params) -> SyncPageNumberPagination[EmailListResponse]
+- client.emails.retrieve_deliveries(id) -> EmailRetrieveDeliveriesResponse
+- client.emails.retry(id) -> EmailRetryResponse
- client.emails.send(\*\*params) -> EmailSendResponse
- client.emails.send_batch(\*\*params) -> EmailSendBatchResponse
- client.emails.send_raw(\*\*params) -> EmailSendRawResponse
diff --git a/src/ark/resources/emails.py b/src/ark/resources/emails.py
index d4eea62..4169d3a 100644
--- a/src/ark/resources/emails.py
+++ b/src/ark/resources/emails.py
@@ -7,7 +7,13 @@
import httpx
-from ..types import email_list_params, email_send_params, email_send_raw_params, email_send_batch_params
+from ..types import (
+ email_list_params,
+ email_send_params,
+ email_retrieve_params,
+ email_send_raw_params,
+ email_send_batch_params,
+)
from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
from .._utils import maybe_transform, strip_not_given, async_maybe_transform
from .._compat import cached_property
@@ -22,8 +28,11 @@
from .._base_client import AsyncPaginator, make_request_options
from ..types.email_list_response import EmailListResponse
from ..types.email_send_response import EmailSendResponse
+from ..types.email_retry_response import EmailRetryResponse
+from ..types.email_retrieve_response import EmailRetrieveResponse
from ..types.email_send_raw_response import EmailSendRawResponse
from ..types.email_send_batch_response import EmailSendBatchResponse
+from ..types.email_retrieve_deliveries_response import EmailRetrieveDeliveriesResponse
__all__ = ["EmailsResource", "AsyncEmailsResource"]
@@ -48,6 +57,59 @@ def with_streaming_response(self) -> EmailsResourceWithStreamingResponse:
"""
return EmailsResourceWithStreamingResponse(self)
+ def retrieve(
+ self,
+ id: str,
+ *,
+ expand: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> EmailRetrieveResponse:
+ """
+ Retrieve detailed information about a specific email including delivery status,
+ timestamps, and optionally the email content.
+
+ Use the `expand` parameter to include additional data like the HTML/text body,
+ headers, or delivery attempts.
+
+ Args:
+ expand:
+ Comma-separated list of fields to include:
+
+ - `full` - Include all expanded fields in a single request
+ - `content` - HTML and plain text body
+ - `headers` - Email headers
+ - `deliveries` - Delivery attempt history
+ - `activity` - Opens and clicks tracking data
+ - `attachments` - File attachments with content (base64 encoded)
+ - `raw` - Complete raw MIME message (base64 encoded)
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._get(
+ f"/emails/{id}",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform({"expand": expand}, email_retrieve_params.EmailRetrieveParams),
+ ),
+ cast_to=EmailRetrieveResponse,
+ )
+
def list(
self,
*,
@@ -136,6 +198,111 @@ def list(
model=EmailListResponse,
)
+ def retrieve_deliveries(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> EmailRetrieveDeliveriesResponse:
+ """
+ Get the complete delivery history for an email, including SMTP response codes,
+ timestamps, and current retry state.
+
+ ## Response Fields
+
+ ### Status
+
+ The current status of the email:
+
+ - `pending` - Awaiting first delivery attempt
+ - `sent` - Successfully delivered to recipient server
+ - `softfail` - Temporary failure, automatic retry scheduled
+ - `hardfail` - Permanent failure, will not retry
+ - `held` - Held for manual review
+ - `bounced` - Bounced by recipient server
+
+ ### Retry State
+
+ When the email is in the delivery queue (`pending` or `softfail` status),
+ `retryState` provides information about the retry schedule:
+
+ - `attempt` - Current attempt number (0 = first attempt)
+ - `maxAttempts` - Maximum attempts before hard-fail (typically 18)
+ - `attemptsRemaining` - Attempts left before hard-fail
+ - `nextRetryAt` - When the next retry is scheduled (Unix timestamp)
+ - `processing` - Whether the email is currently being processed
+ - `manual` - Whether this was triggered by a manual retry
+
+ When the email has finished processing (`sent`, `hardfail`, `held`, `bounced`),
+ `retryState` is `null`.
+
+ ### Can Retry Manually
+
+ Indicates whether you can call `POST /emails/{id}/retry` to manually retry the
+ email. This is `true` when the raw message content is still available (not
+ expired due to retention policy).
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._get(
+ f"/emails/{id}/deliveries",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=EmailRetrieveDeliveriesResponse,
+ )
+
+ def retry(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> EmailRetryResponse:
+ """Retry delivery of a failed or soft-bounced email.
+
+ Creates a new delivery
+ attempt.
+
+ Only works for emails that have failed or are in a retryable state.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._post(
+ f"/emails/{id}/retry",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=EmailRetryResponse,
+ )
+
def send(
self,
*,
@@ -406,6 +573,59 @@ def with_streaming_response(self) -> AsyncEmailsResourceWithStreamingResponse:
"""
return AsyncEmailsResourceWithStreamingResponse(self)
+ async def retrieve(
+ self,
+ id: str,
+ *,
+ expand: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> EmailRetrieveResponse:
+ """
+ Retrieve detailed information about a specific email including delivery status,
+ timestamps, and optionally the email content.
+
+ Use the `expand` parameter to include additional data like the HTML/text body,
+ headers, or delivery attempts.
+
+ Args:
+ expand:
+ Comma-separated list of fields to include:
+
+ - `full` - Include all expanded fields in a single request
+ - `content` - HTML and plain text body
+ - `headers` - Email headers
+ - `deliveries` - Delivery attempt history
+ - `activity` - Opens and clicks tracking data
+ - `attachments` - File attachments with content (base64 encoded)
+ - `raw` - Complete raw MIME message (base64 encoded)
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return await self._get(
+ f"/emails/{id}",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform({"expand": expand}, email_retrieve_params.EmailRetrieveParams),
+ ),
+ cast_to=EmailRetrieveResponse,
+ )
+
def list(
self,
*,
@@ -494,6 +714,111 @@ def list(
model=EmailListResponse,
)
+ async def retrieve_deliveries(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> EmailRetrieveDeliveriesResponse:
+ """
+ Get the complete delivery history for an email, including SMTP response codes,
+ timestamps, and current retry state.
+
+ ## Response Fields
+
+ ### Status
+
+ The current status of the email:
+
+ - `pending` - Awaiting first delivery attempt
+ - `sent` - Successfully delivered to recipient server
+ - `softfail` - Temporary failure, automatic retry scheduled
+ - `hardfail` - Permanent failure, will not retry
+ - `held` - Held for manual review
+ - `bounced` - Bounced by recipient server
+
+ ### Retry State
+
+ When the email is in the delivery queue (`pending` or `softfail` status),
+ `retryState` provides information about the retry schedule:
+
+ - `attempt` - Current attempt number (0 = first attempt)
+ - `maxAttempts` - Maximum attempts before hard-fail (typically 18)
+ - `attemptsRemaining` - Attempts left before hard-fail
+ - `nextRetryAt` - When the next retry is scheduled (Unix timestamp)
+ - `processing` - Whether the email is currently being processed
+ - `manual` - Whether this was triggered by a manual retry
+
+ When the email has finished processing (`sent`, `hardfail`, `held`, `bounced`),
+ `retryState` is `null`.
+
+ ### Can Retry Manually
+
+ Indicates whether you can call `POST /emails/{id}/retry` to manually retry the
+ email. This is `true` when the raw message content is still available (not
+ expired due to retention policy).
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return await self._get(
+ f"/emails/{id}/deliveries",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=EmailRetrieveDeliveriesResponse,
+ )
+
+ async def retry(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> EmailRetryResponse:
+ """Retry delivery of a failed or soft-bounced email.
+
+ Creates a new delivery
+ attempt.
+
+ Only works for emails that have failed or are in a retryable state.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return await self._post(
+ f"/emails/{id}/retry",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=EmailRetryResponse,
+ )
+
async def send(
self,
*,
@@ -748,9 +1073,18 @@ class EmailsResourceWithRawResponse:
def __init__(self, emails: EmailsResource) -> None:
self._emails = emails
+ self.retrieve = to_raw_response_wrapper(
+ emails.retrieve,
+ )
self.list = to_raw_response_wrapper(
emails.list,
)
+ self.retrieve_deliveries = to_raw_response_wrapper(
+ emails.retrieve_deliveries,
+ )
+ self.retry = to_raw_response_wrapper(
+ emails.retry,
+ )
self.send = to_raw_response_wrapper(
emails.send,
)
@@ -766,9 +1100,18 @@ class AsyncEmailsResourceWithRawResponse:
def __init__(self, emails: AsyncEmailsResource) -> None:
self._emails = emails
+ self.retrieve = async_to_raw_response_wrapper(
+ emails.retrieve,
+ )
self.list = async_to_raw_response_wrapper(
emails.list,
)
+ self.retrieve_deliveries = async_to_raw_response_wrapper(
+ emails.retrieve_deliveries,
+ )
+ self.retry = async_to_raw_response_wrapper(
+ emails.retry,
+ )
self.send = async_to_raw_response_wrapper(
emails.send,
)
@@ -784,9 +1127,18 @@ class EmailsResourceWithStreamingResponse:
def __init__(self, emails: EmailsResource) -> None:
self._emails = emails
+ self.retrieve = to_streamed_response_wrapper(
+ emails.retrieve,
+ )
self.list = to_streamed_response_wrapper(
emails.list,
)
+ self.retrieve_deliveries = to_streamed_response_wrapper(
+ emails.retrieve_deliveries,
+ )
+ self.retry = to_streamed_response_wrapper(
+ emails.retry,
+ )
self.send = to_streamed_response_wrapper(
emails.send,
)
@@ -802,9 +1154,18 @@ class AsyncEmailsResourceWithStreamingResponse:
def __init__(self, emails: AsyncEmailsResource) -> None:
self._emails = emails
+ self.retrieve = async_to_streamed_response_wrapper(
+ emails.retrieve,
+ )
self.list = async_to_streamed_response_wrapper(
emails.list,
)
+ self.retrieve_deliveries = async_to_streamed_response_wrapper(
+ emails.retrieve_deliveries,
+ )
+ self.retry = async_to_streamed_response_wrapper(
+ emails.retry,
+ )
self.send = async_to_streamed_response_wrapper(
emails.send,
)
diff --git a/src/ark/resources/webhooks.py b/src/ark/resources/webhooks.py
index f59179e..97b0202 100644
--- a/src/ark/resources/webhooks.py
+++ b/src/ark/resources/webhooks.py
@@ -70,8 +70,6 @@ def create(
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
- "SendLimitApproaching",
- "SendLimitExceeded",
]
]
]
@@ -292,8 +290,6 @@ def list_deliveries(
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
- "SendLimitApproaching",
- "SendLimitExceeded",
]
| Omit = omit,
page: int | Omit = omit,
@@ -478,8 +474,6 @@ def test(
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
- "SendLimitApproaching",
- "SendLimitExceeded",
],
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -563,8 +557,6 @@ async def create(
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
- "SendLimitApproaching",
- "SendLimitExceeded",
]
]
]
@@ -785,8 +777,6 @@ async def list_deliveries(
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
- "SendLimitApproaching",
- "SendLimitExceeded",
]
| Omit = omit,
page: int | Omit = omit,
@@ -971,8 +961,6 @@ async def test(
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
- "SendLimitApproaching",
- "SendLimitExceeded",
],
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
diff --git a/src/ark/types/__init__.py b/src/ark/types/__init__.py
index 5c42442..6cd75fb 100644
--- a/src/ark/types/__init__.py
+++ b/src/ark/types/__init__.py
@@ -15,6 +15,8 @@
from .webhook_test_params import WebhookTestParams as WebhookTestParams
from .domain_create_params import DomainCreateParams as DomainCreateParams
from .domain_list_response import DomainListResponse as DomainListResponse
+from .email_retry_response import EmailRetryResponse as EmailRetryResponse
+from .email_retrieve_params import EmailRetrieveParams as EmailRetrieveParams
from .email_send_raw_params import EmailSendRawParams as EmailSendRawParams
from .log_retrieve_response import LogRetrieveResponse as LogRetrieveResponse
from .webhook_create_params import WebhookCreateParams as WebhookCreateParams
@@ -27,6 +29,7 @@
from .tracking_create_params import TrackingCreateParams as TrackingCreateParams
from .tracking_list_response import TrackingListResponse as TrackingListResponse
from .tracking_update_params import TrackingUpdateParams as TrackingUpdateParams
+from .email_retrieve_response import EmailRetrieveResponse as EmailRetrieveResponse
from .email_send_batch_params import EmailSendBatchParams as EmailSendBatchParams
from .email_send_raw_response import EmailSendRawResponse as EmailSendRawResponse
from .suppression_list_params import SuppressionListParams as SuppressionListParams
@@ -52,4 +55,5 @@
from .suppression_bulk_create_response import SuppressionBulkCreateResponse as SuppressionBulkCreateResponse
from .webhook_list_deliveries_response import WebhookListDeliveriesResponse as WebhookListDeliveriesResponse
from .webhook_replay_delivery_response import WebhookReplayDeliveryResponse as WebhookReplayDeliveryResponse
+from .email_retrieve_deliveries_response import EmailRetrieveDeliveriesResponse as EmailRetrieveDeliveriesResponse
from .webhook_retrieve_delivery_response import WebhookRetrieveDeliveryResponse as WebhookRetrieveDeliveryResponse
diff --git a/src/ark/types/email_list_response.py b/src/ark/types/email_list_response.py
index f8a1d1c..dce7d39 100644
--- a/src/ark/types/email_list_response.py
+++ b/src/ark/types/email_list_response.py
@@ -13,9 +13,7 @@
class EmailListResponse(BaseModel):
id: str
- """Internal message ID"""
-
- token: str
+ """Unique message identifier (token)"""
from_: str = FieldInfo(alias="from")
diff --git a/src/ark/types/email_retrieve_deliveries_response.py b/src/ark/types/email_retrieve_deliveries_response.py
new file mode 100644
index 0000000..6406e9e
--- /dev/null
+++ b/src/ark/types/email_retrieve_deliveries_response.py
@@ -0,0 +1,130 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from .._models import BaseModel
+from .shared.api_meta import APIMeta
+
+__all__ = ["EmailRetrieveDeliveriesResponse", "Data", "DataDelivery", "DataRetryState"]
+
+
+class DataDelivery(BaseModel):
+ id: str
+ """Delivery attempt ID"""
+
+ status: str
+ """Delivery status (lowercase)"""
+
+ timestamp: float
+ """Unix timestamp"""
+
+ timestamp_iso: datetime = FieldInfo(alias="timestampIso")
+ """ISO 8601 timestamp"""
+
+ code: Optional[int] = None
+ """SMTP response code"""
+
+ details: Optional[str] = None
+ """Status details"""
+
+ output: Optional[str] = None
+ """SMTP server response from the receiving mail server"""
+
+ sent_with_ssl: Optional[bool] = FieldInfo(alias="sentWithSsl", default=None)
+ """Whether TLS was used"""
+
+
+class DataRetryState(BaseModel):
+ """
+ Information about the current retry state of a message that is queued for delivery.
+ Only present when the message is in the delivery queue.
+ """
+
+ attempt: int
+ """Current attempt number (0-indexed).
+
+ The first delivery attempt is 0, the first retry is 1, and so on.
+ """
+
+ attempts_remaining: int = FieldInfo(alias="attemptsRemaining")
+ """
+ Number of attempts remaining before the message is hard-failed. Calculated as
+ `maxAttempts - attempt`.
+ """
+
+ manual: bool
+ """
+ Whether this queue entry was created by a manual retry request. Manual retries
+ bypass certain hold conditions like suppression lists.
+ """
+
+ max_attempts: int = FieldInfo(alias="maxAttempts")
+ """
+ Maximum number of delivery attempts before the message is hard-failed.
+ Configured at the server level.
+ """
+
+ processing: bool
+ """
+ Whether the message is currently being processed by a delivery worker. When
+ `true`, the message is actively being sent.
+ """
+
+ next_retry_at: Optional[float] = FieldInfo(alias="nextRetryAt", default=None)
+ """
+ Unix timestamp of when the next retry attempt is scheduled. `null` if the
+ message is ready for immediate processing or currently being processed.
+ """
+
+ next_retry_at_iso: Optional[datetime] = FieldInfo(alias="nextRetryAtIso", default=None)
+ """
+ ISO 8601 formatted timestamp of the next retry attempt. `null` if the message is
+ ready for immediate processing.
+ """
+
+
+class Data(BaseModel):
+ id: str
+ """Message identifier (token)"""
+
+ can_retry_manually: bool = FieldInfo(alias="canRetryManually")
+ """
+ Whether the message can be manually retried via `POST /emails/{id}/retry`.
+ `true` when the raw message content is still available (not expired). Messages
+ older than the retention period cannot be retried.
+ """
+
+ deliveries: List[DataDelivery]
+ """
+ Chronological list of delivery attempts for this message. Each attempt includes
+ SMTP response codes and timestamps.
+ """
+
+ retry_state: Optional[DataRetryState] = FieldInfo(alias="retryState", default=None)
+ """
+ Information about the current retry state of a message that is queued for
+ delivery. Only present when the message is in the delivery queue.
+ """
+
+ status: Literal["pending", "sent", "softfail", "hardfail", "held", "bounced"]
+ """Current message status (lowercase). Possible values:
+
+ - `pending` - Initial state, awaiting first delivery attempt
+ - `sent` - Successfully delivered
+ - `softfail` - Temporary failure, will retry automatically
+ - `hardfail` - Permanent failure, will not retry
+ - `held` - Held for manual review (suppression list, etc.)
+ - `bounced` - Bounced by recipient server
+ """
+
+
+class EmailRetrieveDeliveriesResponse(BaseModel):
+ data: Data
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/src/ark/types/email_retrieve_params.py b/src/ark/types/email_retrieve_params.py
new file mode 100644
index 0000000..5d3fc43
--- /dev/null
+++ b/src/ark/types/email_retrieve_params.py
@@ -0,0 +1,21 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["EmailRetrieveParams"]
+
+
+class EmailRetrieveParams(TypedDict, total=False):
+ expand: str
+ """Comma-separated list of fields to include:
+
+ - `full` - Include all expanded fields in a single request
+ - `content` - HTML and plain text body
+ - `headers` - Email headers
+ - `deliveries` - Delivery attempt history
+ - `activity` - Opens and clicks tracking data
+ - `attachments` - File attachments with content (base64 encoded)
+ - `raw` - Complete raw MIME message (base64 encoded)
+ """
diff --git a/src/ark/types/email_retrieve_response.py b/src/ark/types/email_retrieve_response.py
new file mode 100644
index 0000000..f60801e
--- /dev/null
+++ b/src/ark/types/email_retrieve_response.py
@@ -0,0 +1,184 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, List, Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from .._models import BaseModel
+from .shared.api_meta import APIMeta
+
+__all__ = [
+ "EmailRetrieveResponse",
+ "Data",
+ "DataActivity",
+ "DataActivityClick",
+ "DataActivityOpen",
+ "DataAttachment",
+ "DataDelivery",
+]
+
+
+class DataActivityClick(BaseModel):
+ ip_address: Optional[str] = FieldInfo(alias="ipAddress", default=None)
+ """IP address of the clicker"""
+
+ timestamp: Optional[float] = None
+ """Unix timestamp of the click event"""
+
+ timestamp_iso: Optional[datetime] = FieldInfo(alias="timestampIso", default=None)
+ """ISO 8601 timestamp of the click event"""
+
+ url: Optional[str] = None
+ """URL that was clicked"""
+
+ user_agent: Optional[str] = FieldInfo(alias="userAgent", default=None)
+ """User agent of the email client"""
+
+
+class DataActivityOpen(BaseModel):
+ ip_address: Optional[str] = FieldInfo(alias="ipAddress", default=None)
+ """IP address of the opener"""
+
+ timestamp: Optional[float] = None
+ """Unix timestamp of the open event"""
+
+ timestamp_iso: Optional[datetime] = FieldInfo(alias="timestampIso", default=None)
+ """ISO 8601 timestamp of the open event"""
+
+ user_agent: Optional[str] = FieldInfo(alias="userAgent", default=None)
+ """User agent of the email client"""
+
+
+class DataActivity(BaseModel):
+ """Opens and clicks tracking data (included if expand=activity)"""
+
+ clicks: Optional[List[DataActivityClick]] = None
+ """List of link click events"""
+
+ opens: Optional[List[DataActivityOpen]] = None
+ """List of email open events"""
+
+
+class DataAttachment(BaseModel):
+ """An email attachment retrieved from a sent message"""
+
+ content_type: str = FieldInfo(alias="contentType")
+ """MIME type of the attachment"""
+
+ data: str
+ """Base64 encoded attachment content. Decode this to get the raw file bytes."""
+
+ filename: str
+ """Original filename of the attachment"""
+
+ hash: str
+ """SHA256 hash of the attachment content for verification"""
+
+ size: int
+ """Size of the attachment in bytes"""
+
+
+class DataDelivery(BaseModel):
+ id: str
+ """Delivery attempt ID"""
+
+ status: str
+ """Delivery status (lowercase)"""
+
+ timestamp: float
+ """Unix timestamp"""
+
+ timestamp_iso: datetime = FieldInfo(alias="timestampIso")
+ """ISO 8601 timestamp"""
+
+ code: Optional[int] = None
+ """SMTP response code"""
+
+ details: Optional[str] = None
+ """Status details"""
+
+ output: Optional[str] = None
+ """SMTP server response from the receiving mail server"""
+
+ sent_with_ssl: Optional[bool] = FieldInfo(alias="sentWithSsl", default=None)
+ """Whether TLS was used"""
+
+
+class Data(BaseModel):
+ id: str
+ """Unique message identifier (token)"""
+
+ from_: str = FieldInfo(alias="from")
+ """Sender address"""
+
+ scope: Literal["outgoing", "incoming"]
+ """Message direction"""
+
+ status: Literal["pending", "sent", "softfail", "hardfail", "bounced", "held"]
+ """Current delivery status:
+
+ - `pending` - Email accepted, waiting to be processed
+ - `sent` - Email transmitted to recipient's mail server
+ - `softfail` - Temporary delivery failure, will retry
+ - `hardfail` - Permanent delivery failure
+ - `bounced` - Email bounced back
+ - `held` - Held for manual review
+ """
+
+ subject: str
+ """Email subject line"""
+
+ timestamp: float
+ """Unix timestamp when the email was sent"""
+
+ timestamp_iso: datetime = FieldInfo(alias="timestampIso")
+ """ISO 8601 formatted timestamp"""
+
+ to: str
+ """Recipient address"""
+
+ activity: Optional[DataActivity] = None
+ """Opens and clicks tracking data (included if expand=activity)"""
+
+ attachments: Optional[List[DataAttachment]] = None
+ """File attachments (included if expand=attachments)"""
+
+ deliveries: Optional[List[DataDelivery]] = None
+ """Delivery attempt history (included if expand=deliveries)"""
+
+ headers: Optional[Dict[str, str]] = None
+ """Email headers (included if expand=headers)"""
+
+ html_body: Optional[str] = FieldInfo(alias="htmlBody", default=None)
+ """HTML body content (included if expand=content)"""
+
+ message_id: Optional[str] = FieldInfo(alias="messageId", default=None)
+ """SMTP Message-ID header"""
+
+ plain_body: Optional[str] = FieldInfo(alias="plainBody", default=None)
+ """Plain text body (included if expand=content)"""
+
+ raw_message: Optional[str] = FieldInfo(alias="rawMessage", default=None)
+ """
+ Complete raw MIME message, base64 encoded (included if expand=raw). Decode this
+ to get the original RFC 2822 formatted email.
+ """
+
+ spam: Optional[bool] = None
+ """Whether the message was flagged as spam"""
+
+ spam_score: Optional[float] = FieldInfo(alias="spamScore", default=None)
+ """Spam score (if applicable)"""
+
+ tag: Optional[str] = None
+ """Optional categorization tag"""
+
+
+class EmailRetrieveResponse(BaseModel):
+ data: Data
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/src/ark/types/email_retry_response.py b/src/ark/types/email_retry_response.py
new file mode 100644
index 0000000..f63cc99
--- /dev/null
+++ b/src/ark/types/email_retry_response.py
@@ -0,0 +1,23 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from .._models import BaseModel
+from .shared.api_meta import APIMeta
+
+__all__ = ["EmailRetryResponse", "Data"]
+
+
+class Data(BaseModel):
+ id: str
+ """Email identifier (token)"""
+
+ message: str
+
+
+class EmailRetryResponse(BaseModel):
+ data: Data
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/src/ark/types/email_send_batch_response.py b/src/ark/types/email_send_batch_response.py
index 272119c..d32e3fc 100644
--- a/src/ark/types/email_send_batch_response.py
+++ b/src/ark/types/email_send_batch_response.py
@@ -11,9 +11,7 @@
class DataMessages(BaseModel):
id: str
- """Message ID"""
-
- token: str
+ """Message identifier (token)"""
class Data(BaseModel):
diff --git a/src/ark/types/email_send_raw_response.py b/src/ark/types/email_send_raw_response.py
index f1e8c03..46e7c96 100644
--- a/src/ark/types/email_send_raw_response.py
+++ b/src/ark/types/email_send_raw_response.py
@@ -13,7 +13,7 @@
class Data(BaseModel):
id: str
- """Unique message ID (format: msg*{id}*{token})"""
+ """Unique message identifier (token)"""
status: Literal["pending", "sent"]
"""Current delivery status"""
diff --git a/src/ark/types/email_send_response.py b/src/ark/types/email_send_response.py
index 39e1d36..cb7d814 100644
--- a/src/ark/types/email_send_response.py
+++ b/src/ark/types/email_send_response.py
@@ -13,7 +13,7 @@
class Data(BaseModel):
id: str
- """Unique message ID (format: msg*{id}*{token})"""
+ """Unique message identifier (token)"""
status: Literal["pending", "sent"]
"""Current delivery status"""
diff --git a/src/ark/types/log_entry.py b/src/ark/types/log_entry.py
index 9fad7e7..4dfdd6c 100644
--- a/src/ark/types/log_entry.py
+++ b/src/ark/types/log_entry.py
@@ -57,7 +57,7 @@ class Email(BaseModel):
"""Email-specific data (for email endpoints)"""
id: Optional[str] = None
- """Email message ID"""
+ """Email message identifier (token)"""
recipient_count: Optional[int] = FieldInfo(alias="recipientCount", default=None)
"""Number of recipients"""
diff --git a/src/ark/types/webhook_create_params.py b/src/ark/types/webhook_create_params.py
index 8b7a63e..9899826 100644
--- a/src/ark/types/webhook_create_params.py
+++ b/src/ark/types/webhook_create_params.py
@@ -34,8 +34,6 @@ class WebhookCreateParams(TypedDict, total=False):
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
- "SendLimitApproaching",
- "SendLimitExceeded",
]
]
]
diff --git a/src/ark/types/webhook_create_response.py b/src/ark/types/webhook_create_response.py
index 07f75ec..d78bb58 100644
--- a/src/ark/types/webhook_create_response.py
+++ b/src/ark/types/webhook_create_response.py
@@ -34,8 +34,6 @@ class Data(BaseModel):
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
- "SendLimitApproaching",
- "SendLimitExceeded",
]
]
"""Subscribed events"""
diff --git a/src/ark/types/webhook_list_deliveries_params.py b/src/ark/types/webhook_list_deliveries_params.py
index cce2c61..c4048e4 100644
--- a/src/ark/types/webhook_list_deliveries_params.py
+++ b/src/ark/types/webhook_list_deliveries_params.py
@@ -25,8 +25,6 @@ class WebhookListDeliveriesParams(TypedDict, total=False):
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
- "SendLimitApproaching",
- "SendLimitExceeded",
]
"""Filter by event type"""
diff --git a/src/ark/types/webhook_list_deliveries_response.py b/src/ark/types/webhook_list_deliveries_response.py
index cb5033d..71a5fe2 100644
--- a/src/ark/types/webhook_list_deliveries_response.py
+++ b/src/ark/types/webhook_list_deliveries_response.py
@@ -30,8 +30,6 @@ class Data(BaseModel):
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
- "SendLimitApproaching",
- "SendLimitExceeded",
]
"""Event type that triggered this delivery"""
diff --git a/src/ark/types/webhook_retrieve_delivery_response.py b/src/ark/types/webhook_retrieve_delivery_response.py
index 9a0c38b..efec5c4 100644
--- a/src/ark/types/webhook_retrieve_delivery_response.py
+++ b/src/ark/types/webhook_retrieve_delivery_response.py
@@ -50,8 +50,6 @@ class Data(BaseModel):
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
- "SendLimitApproaching",
- "SendLimitExceeded",
]
"""Event type that triggered this delivery"""
diff --git a/src/ark/types/webhook_retrieve_response.py b/src/ark/types/webhook_retrieve_response.py
index db780ca..6cb2f2e 100644
--- a/src/ark/types/webhook_retrieve_response.py
+++ b/src/ark/types/webhook_retrieve_response.py
@@ -34,8 +34,6 @@ class Data(BaseModel):
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
- "SendLimitApproaching",
- "SendLimitExceeded",
]
]
"""Subscribed events"""
diff --git a/src/ark/types/webhook_test_params.py b/src/ark/types/webhook_test_params.py
index 5648540..3f91fca 100644
--- a/src/ark/types/webhook_test_params.py
+++ b/src/ark/types/webhook_test_params.py
@@ -18,8 +18,6 @@ class WebhookTestParams(TypedDict, total=False):
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
- "SendLimitApproaching",
- "SendLimitExceeded",
]
]
"""Event type to simulate"""
diff --git a/src/ark/types/webhook_update_response.py b/src/ark/types/webhook_update_response.py
index 75c6d59..4d9a0c6 100644
--- a/src/ark/types/webhook_update_response.py
+++ b/src/ark/types/webhook_update_response.py
@@ -34,8 +34,6 @@ class Data(BaseModel):
"MessageLinkClicked",
"MessageLoaded",
"DomainDNSError",
- "SendLimitApproaching",
- "SendLimitExceeded",
]
]
"""Subscribed events"""
diff --git a/tests/api_resources/test_emails.py b/tests/api_resources/test_emails.py
index e416353..7a54df7 100644
--- a/tests/api_resources/test_emails.py
+++ b/tests/api_resources/test_emails.py
@@ -11,8 +11,11 @@
from ark.types import (
EmailListResponse,
EmailSendResponse,
+ EmailRetryResponse,
EmailSendRawResponse,
+ EmailRetrieveResponse,
EmailSendBatchResponse,
+ EmailRetrieveDeliveriesResponse,
)
from tests.utils import assert_matches_type
from ark.pagination import SyncPageNumberPagination, AsyncPageNumberPagination
@@ -23,6 +26,52 @@
class TestEmails:
parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+ @parametrize
+ def test_method_retrieve(self, client: Ark) -> None:
+ email = client.emails.retrieve(
+ id="aBc123XyZ",
+ )
+ assert_matches_type(EmailRetrieveResponse, email, path=["response"])
+
+ @parametrize
+ def test_method_retrieve_with_all_params(self, client: Ark) -> None:
+ email = client.emails.retrieve(
+ id="aBc123XyZ",
+ expand="full",
+ )
+ assert_matches_type(EmailRetrieveResponse, email, path=["response"])
+
+ @parametrize
+ def test_raw_response_retrieve(self, client: Ark) -> None:
+ response = client.emails.with_raw_response.retrieve(
+ id="aBc123XyZ",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ email = response.parse()
+ assert_matches_type(EmailRetrieveResponse, email, path=["response"])
+
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Ark) -> None:
+ with client.emails.with_streaming_response.retrieve(
+ id="aBc123XyZ",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ email = response.parse()
+ assert_matches_type(EmailRetrieveResponse, email, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_retrieve(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.emails.with_raw_response.retrieve(
+ id="",
+ )
+
@parametrize
def test_method_list(self, client: Ark) -> None:
email = client.emails.list()
@@ -62,6 +111,82 @@ def test_streaming_response_list(self, client: Ark) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ def test_method_retrieve_deliveries(self, client: Ark) -> None:
+ email = client.emails.retrieve_deliveries(
+ "aBc123XyZ",
+ )
+ assert_matches_type(EmailRetrieveDeliveriesResponse, email, path=["response"])
+
+ @parametrize
+ def test_raw_response_retrieve_deliveries(self, client: Ark) -> None:
+ response = client.emails.with_raw_response.retrieve_deliveries(
+ "aBc123XyZ",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ email = response.parse()
+ assert_matches_type(EmailRetrieveDeliveriesResponse, email, path=["response"])
+
+ @parametrize
+ def test_streaming_response_retrieve_deliveries(self, client: Ark) -> None:
+ with client.emails.with_streaming_response.retrieve_deliveries(
+ "aBc123XyZ",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ email = response.parse()
+ assert_matches_type(EmailRetrieveDeliveriesResponse, email, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_retrieve_deliveries(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.emails.with_raw_response.retrieve_deliveries(
+ "",
+ )
+
+ @parametrize
+ def test_method_retry(self, client: Ark) -> None:
+ email = client.emails.retry(
+ "aBc123XyZ",
+ )
+ assert_matches_type(EmailRetryResponse, email, path=["response"])
+
+ @parametrize
+ def test_raw_response_retry(self, client: Ark) -> None:
+ response = client.emails.with_raw_response.retry(
+ "aBc123XyZ",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ email = response.parse()
+ assert_matches_type(EmailRetryResponse, email, path=["response"])
+
+ @parametrize
+ def test_streaming_response_retry(self, client: Ark) -> None:
+ with client.emails.with_streaming_response.retry(
+ "aBc123XyZ",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ email = response.parse()
+ assert_matches_type(EmailRetryResponse, email, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_retry(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.emails.with_raw_response.retry(
+ "",
+ )
+
@parametrize
def test_method_send(self, client: Ark) -> None:
email = client.emails.send(
@@ -275,6 +400,52 @@ class TestAsyncEmails:
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
)
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncArk) -> None:
+ email = await async_client.emails.retrieve(
+ id="aBc123XyZ",
+ )
+ assert_matches_type(EmailRetrieveResponse, email, path=["response"])
+
+ @parametrize
+ async def test_method_retrieve_with_all_params(self, async_client: AsyncArk) -> None:
+ email = await async_client.emails.retrieve(
+ id="aBc123XyZ",
+ expand="full",
+ )
+ assert_matches_type(EmailRetrieveResponse, email, path=["response"])
+
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncArk) -> None:
+ response = await async_client.emails.with_raw_response.retrieve(
+ id="aBc123XyZ",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ email = await response.parse()
+ assert_matches_type(EmailRetrieveResponse, email, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncArk) -> None:
+ async with async_client.emails.with_streaming_response.retrieve(
+ id="aBc123XyZ",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ email = await response.parse()
+ assert_matches_type(EmailRetrieveResponse, email, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_retrieve(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.emails.with_raw_response.retrieve(
+ id="",
+ )
+
@parametrize
async def test_method_list(self, async_client: AsyncArk) -> None:
email = await async_client.emails.list()
@@ -314,6 +485,82 @@ async def test_streaming_response_list(self, async_client: AsyncArk) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ async def test_method_retrieve_deliveries(self, async_client: AsyncArk) -> None:
+ email = await async_client.emails.retrieve_deliveries(
+ "aBc123XyZ",
+ )
+ assert_matches_type(EmailRetrieveDeliveriesResponse, email, path=["response"])
+
+ @parametrize
+ async def test_raw_response_retrieve_deliveries(self, async_client: AsyncArk) -> None:
+ response = await async_client.emails.with_raw_response.retrieve_deliveries(
+ "aBc123XyZ",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ email = await response.parse()
+ assert_matches_type(EmailRetrieveDeliveriesResponse, email, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_retrieve_deliveries(self, async_client: AsyncArk) -> None:
+ async with async_client.emails.with_streaming_response.retrieve_deliveries(
+ "aBc123XyZ",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ email = await response.parse()
+ assert_matches_type(EmailRetrieveDeliveriesResponse, email, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_retrieve_deliveries(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.emails.with_raw_response.retrieve_deliveries(
+ "",
+ )
+
+ @parametrize
+ async def test_method_retry(self, async_client: AsyncArk) -> None:
+ email = await async_client.emails.retry(
+ "aBc123XyZ",
+ )
+ assert_matches_type(EmailRetryResponse, email, path=["response"])
+
+ @parametrize
+ async def test_raw_response_retry(self, async_client: AsyncArk) -> None:
+ response = await async_client.emails.with_raw_response.retry(
+ "aBc123XyZ",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ email = await response.parse()
+ assert_matches_type(EmailRetryResponse, email, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_retry(self, async_client: AsyncArk) -> None:
+ async with async_client.emails.with_streaming_response.retry(
+ "aBc123XyZ",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ email = await response.parse()
+ assert_matches_type(EmailRetryResponse, email, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_retry(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.emails.with_raw_response.retry(
+ "",
+ )
+
@parametrize
async def test_method_send(self, async_client: AsyncArk) -> None:
email = await async_client.emails.send(
From 0e5c6fe1479ca123629e23c842fc3cdf231876e5 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 2 Feb 2026 22:10:08 +0000
Subject: [PATCH 3/6] feat(api): manual updates
---
.stats.yml | 4 ++--
src/ark/types/email_retrieve_deliveries_response.py | 9 +++++++--
src/ark/types/email_retrieve_response.py | 9 +++++++--
3 files changed, 16 insertions(+), 6 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 17e53d5..02f1977 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 35
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-13bda63d06cee34ce339a861539358c6322e3c3964b2ef9994621de91138ef35.yml
-openapi_spec_hash: db21296328b817af863e09c31596316f
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-db6ffc6e3ecc5ea44062d5777ed1a0cd229af83b6af32584c3e9d3a074ae0ae6.yml
+openapi_spec_hash: bd5879cb48cf078c0be066574931ee3a
config_hash: 77a3908ee910a8019f5831d3a3d53c18
diff --git a/src/ark/types/email_retrieve_deliveries_response.py b/src/ark/types/email_retrieve_deliveries_response.py
index 6406e9e..b900e5a 100644
--- a/src/ark/types/email_retrieve_deliveries_response.py
+++ b/src/ark/types/email_retrieve_deliveries_response.py
@@ -29,10 +29,15 @@ class DataDelivery(BaseModel):
"""SMTP response code"""
details: Optional[str] = None
- """Status details"""
+ """Human-readable delivery summary. Format varies by status:
+
+ - **sent**: `Message for {recipient} accepted by {ip}:{port} ({hostname})`
+ - **softfail/hardfail**:
+ `{code} {classification}: Delivery to {recipient} failed at {ip}:{port} ({hostname})`
+ """
output: Optional[str] = None
- """SMTP server response from the receiving mail server"""
+ """Raw SMTP response from the receiving mail server"""
sent_with_ssl: Optional[bool] = FieldInfo(alias="sentWithSsl", default=None)
"""Whether TLS was used"""
diff --git a/src/ark/types/email_retrieve_response.py b/src/ark/types/email_retrieve_response.py
index f60801e..41e7ef2 100644
--- a/src/ark/types/email_retrieve_response.py
+++ b/src/ark/types/email_retrieve_response.py
@@ -97,10 +97,15 @@ class DataDelivery(BaseModel):
"""SMTP response code"""
details: Optional[str] = None
- """Status details"""
+ """Human-readable delivery summary. Format varies by status:
+
+ - **sent**: `Message for {recipient} accepted by {ip}:{port} ({hostname})`
+ - **softfail/hardfail**:
+ `{code} {classification}: Delivery to {recipient} failed at {ip}:{port} ({hostname})`
+ """
output: Optional[str] = None
- """SMTP server response from the receiving mail server"""
+ """Raw SMTP response from the receiving mail server"""
sent_with_ssl: Optional[bool] = FieldInfo(alias="sentWithSsl", default=None)
"""Whether TLS was used"""
From f355920e2008cf0d5990b15c7292483279ee671c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 2 Feb 2026 22:46:59 +0000
Subject: [PATCH 4/6] feat(api): manual updates
---
.stats.yml | 4 +-
.../email_retrieve_deliveries_response.py | 44 +++++++++++++++++++
src/ark/types/email_retrieve_response.py | 44 +++++++++++++++++++
3 files changed, 90 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 02f1977..c013b63 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 35
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-db6ffc6e3ecc5ea44062d5777ed1a0cd229af83b6af32584c3e9d3a074ae0ae6.yml
-openapi_spec_hash: bd5879cb48cf078c0be066574931ee3a
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-724e563a8852343de11e72cb6886923e5e530ab59992ca25ce8717874ca797f7.yml
+openapi_spec_hash: d02f255aee89b99bb0e4043600c745e9
config_hash: 77a3908ee910a8019f5831d3a3d53c18
diff --git a/src/ark/types/email_retrieve_deliveries_response.py b/src/ark/types/email_retrieve_deliveries_response.py
index b900e5a..a591b9d 100644
--- a/src/ark/types/email_retrieve_deliveries_response.py
+++ b/src/ark/types/email_retrieve_deliveries_response.py
@@ -25,6 +25,37 @@ class DataDelivery(BaseModel):
timestamp_iso: datetime = FieldInfo(alias="timestampIso")
"""ISO 8601 timestamp"""
+ classification: Optional[
+ Literal[
+ "invalid_recipient",
+ "mailbox_full",
+ "message_too_large",
+ "spam_block",
+ "policy_violation",
+ "no_mailbox",
+ "not_accepting_mail",
+ "temporarily_unavailable",
+ "protocol_error",
+ "tls_required",
+ "connection_error",
+ "dns_error",
+ "unclassified",
+ ]
+ ] = None
+ """
+ Bounce classification category (present for failed deliveries). Helps understand
+ why delivery failed for analytics and automated handling.
+ """
+
+ classification_code: Optional[int] = FieldInfo(alias="classificationCode", default=None)
+ """
+ Numeric bounce classification code for programmatic handling. Codes:
+ 10=invalid_recipient, 11=no_mailbox, 12=not_accepting_mail, 20=mailbox_full,
+ 21=message_too_large, 30=spam_block, 31=policy_violation, 32=tls_required,
+ 40=connection_error, 41=dns_error, 42=temporarily_unavailable,
+ 50=protocol_error, 99=unclassified
+ """
+
code: Optional[int] = None
"""SMTP response code"""
@@ -39,9 +70,22 @@ class DataDelivery(BaseModel):
output: Optional[str] = None
"""Raw SMTP response from the receiving mail server"""
+ remote_host: Optional[str] = FieldInfo(alias="remoteHost", default=None)
+ """
+ Hostname of the remote mail server that processed the delivery. Present for all
+ delivery attempts (successful and failed).
+ """
+
sent_with_ssl: Optional[bool] = FieldInfo(alias="sentWithSsl", default=None)
"""Whether TLS was used"""
+ smtp_enhanced_code: Optional[str] = FieldInfo(alias="smtpEnhancedCode", default=None)
+ """
+ RFC 3463 enhanced status code from SMTP response (e.g., "5.1.1", "4.2.2"). First
+ digit: 2=success, 4=temporary, 5=permanent. Second digit: category (1=address,
+ 2=mailbox, 7=security, etc.).
+ """
+
class DataRetryState(BaseModel):
"""
diff --git a/src/ark/types/email_retrieve_response.py b/src/ark/types/email_retrieve_response.py
index 41e7ef2..d9bc54c 100644
--- a/src/ark/types/email_retrieve_response.py
+++ b/src/ark/types/email_retrieve_response.py
@@ -93,6 +93,37 @@ class DataDelivery(BaseModel):
timestamp_iso: datetime = FieldInfo(alias="timestampIso")
"""ISO 8601 timestamp"""
+ classification: Optional[
+ Literal[
+ "invalid_recipient",
+ "mailbox_full",
+ "message_too_large",
+ "spam_block",
+ "policy_violation",
+ "no_mailbox",
+ "not_accepting_mail",
+ "temporarily_unavailable",
+ "protocol_error",
+ "tls_required",
+ "connection_error",
+ "dns_error",
+ "unclassified",
+ ]
+ ] = None
+ """
+ Bounce classification category (present for failed deliveries). Helps understand
+ why delivery failed for analytics and automated handling.
+ """
+
+ classification_code: Optional[int] = FieldInfo(alias="classificationCode", default=None)
+ """
+ Numeric bounce classification code for programmatic handling. Codes:
+ 10=invalid_recipient, 11=no_mailbox, 12=not_accepting_mail, 20=mailbox_full,
+ 21=message_too_large, 30=spam_block, 31=policy_violation, 32=tls_required,
+ 40=connection_error, 41=dns_error, 42=temporarily_unavailable,
+ 50=protocol_error, 99=unclassified
+ """
+
code: Optional[int] = None
"""SMTP response code"""
@@ -107,9 +138,22 @@ class DataDelivery(BaseModel):
output: Optional[str] = None
"""Raw SMTP response from the receiving mail server"""
+ remote_host: Optional[str] = FieldInfo(alias="remoteHost", default=None)
+ """
+ Hostname of the remote mail server that processed the delivery. Present for all
+ delivery attempts (successful and failed).
+ """
+
sent_with_ssl: Optional[bool] = FieldInfo(alias="sentWithSsl", default=None)
"""Whether TLS was used"""
+ smtp_enhanced_code: Optional[str] = FieldInfo(alias="smtpEnhancedCode", default=None)
+ """
+ RFC 3463 enhanced status code from SMTP response (e.g., "5.1.1", "4.2.2"). First
+ digit: 2=success, 4=temporary, 5=permanent. Second digit: category (1=address,
+ 2=mailbox, 7=security, etc.).
+ """
+
class Data(BaseModel):
id: str
From 8ba85ac06c8e1d803f2cf5077a1ff99d4655e178 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 3 Feb 2026 10:30:33 +0000
Subject: [PATCH 5/6] feat(api): Add Tenants
---
.stats.yml | 8 +-
api.md | 22 +
src/ark/_client.py | 39 +-
src/ark/resources/__init__.py | 14 +
src/ark/resources/tenants.py | 621 ++++++++++++++++++++++
src/ark/types/__init__.py | 8 +
src/ark/types/tenant.py | 34 ++
src/ark/types/tenant_create_params.py | 24 +
src/ark/types/tenant_create_response.py | 17 +
src/ark/types/tenant_delete_response.py | 20 +
src/ark/types/tenant_list_params.py | 20 +
src/ark/types/tenant_retrieve_response.py | 17 +
src/ark/types/tenant_update_params.py | 27 +
src/ark/types/tenant_update_response.py | 17 +
tests/api_resources/test_tenants.py | 441 +++++++++++++++
15 files changed, 1324 insertions(+), 5 deletions(-)
create mode 100644 src/ark/resources/tenants.py
create mode 100644 src/ark/types/tenant.py
create mode 100644 src/ark/types/tenant_create_params.py
create mode 100644 src/ark/types/tenant_create_response.py
create mode 100644 src/ark/types/tenant_delete_response.py
create mode 100644 src/ark/types/tenant_list_params.py
create mode 100644 src/ark/types/tenant_retrieve_response.py
create mode 100644 src/ark/types/tenant_update_params.py
create mode 100644 src/ark/types/tenant_update_response.py
create mode 100644 tests/api_resources/test_tenants.py
diff --git a/.stats.yml b/.stats.yml
index c013b63..576e5df 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 35
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-724e563a8852343de11e72cb6886923e5e530ab59992ca25ce8717874ca797f7.yml
-openapi_spec_hash: d02f255aee89b99bb0e4043600c745e9
-config_hash: 77a3908ee910a8019f5831d3a3d53c18
+configured_endpoints: 40
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-628db0b9b7c9da594fa6ad6ce9d95f4ecad92c9e0313f2f1f9977216494dbc5d.yml
+openapi_spec_hash: 1773341fbff31b84d2cbcdb37eaad877
+config_hash: b090c2bdd7a719c56c825edddc587737
diff --git a/api.md b/api.md
index 7baaad3..19e5c9f 100644
--- a/api.md
+++ b/api.md
@@ -154,3 +154,25 @@ from ark.types import UsageRetrieveResponse
Methods:
- client.usage.retrieve() -> UsageRetrieveResponse
+
+# Tenants
+
+Types:
+
+```python
+from ark.types import (
+ Tenant,
+ TenantCreateResponse,
+ TenantRetrieveResponse,
+ TenantUpdateResponse,
+ TenantDeleteResponse,
+)
+```
+
+Methods:
+
+- client.tenants.create(\*\*params) -> TenantCreateResponse
+- client.tenants.retrieve(tenant_id) -> TenantRetrieveResponse
+- client.tenants.update(tenant_id, \*\*params) -> TenantUpdateResponse
+- client.tenants.list(\*\*params) -> SyncPageNumberPagination[Tenant]
+- client.tenants.delete(tenant_id) -> TenantDeleteResponse
diff --git a/src/ark/_client.py b/src/ark/_client.py
index 4a2f2a8..51fb70d 100644
--- a/src/ark/_client.py
+++ b/src/ark/_client.py
@@ -31,11 +31,12 @@
)
if TYPE_CHECKING:
- from .resources import logs, usage, emails, domains, tracking, webhooks, suppressions
+ from .resources import logs, usage, emails, domains, tenants, tracking, webhooks, suppressions
from .resources.logs import LogsResource, AsyncLogsResource
from .resources.usage import UsageResource, AsyncUsageResource
from .resources.emails import EmailsResource, AsyncEmailsResource
from .resources.domains import DomainsResource, AsyncDomainsResource
+ from .resources.tenants import TenantsResource, AsyncTenantsResource
from .resources.tracking import TrackingResource, AsyncTrackingResource
from .resources.webhooks import WebhooksResource, AsyncWebhooksResource
from .resources.suppressions import SuppressionsResource, AsyncSuppressionsResource
@@ -140,6 +141,12 @@ def usage(self) -> UsageResource:
return UsageResource(self)
+ @cached_property
+ def tenants(self) -> TenantsResource:
+ from .resources.tenants import TenantsResource
+
+ return TenantsResource(self)
+
@cached_property
def with_raw_response(self) -> ArkWithRawResponse:
return ArkWithRawResponse(self)
@@ -350,6 +357,12 @@ def usage(self) -> AsyncUsageResource:
return AsyncUsageResource(self)
+ @cached_property
+ def tenants(self) -> AsyncTenantsResource:
+ from .resources.tenants import AsyncTenantsResource
+
+ return AsyncTenantsResource(self)
+
@cached_property
def with_raw_response(self) -> AsyncArkWithRawResponse:
return AsyncArkWithRawResponse(self)
@@ -511,6 +524,12 @@ def usage(self) -> usage.UsageResourceWithRawResponse:
return UsageResourceWithRawResponse(self._client.usage)
+ @cached_property
+ def tenants(self) -> tenants.TenantsResourceWithRawResponse:
+ from .resources.tenants import TenantsResourceWithRawResponse
+
+ return TenantsResourceWithRawResponse(self._client.tenants)
+
class AsyncArkWithRawResponse:
_client: AsyncArk
@@ -560,6 +579,12 @@ def usage(self) -> usage.AsyncUsageResourceWithRawResponse:
return AsyncUsageResourceWithRawResponse(self._client.usage)
+ @cached_property
+ def tenants(self) -> tenants.AsyncTenantsResourceWithRawResponse:
+ from .resources.tenants import AsyncTenantsResourceWithRawResponse
+
+ return AsyncTenantsResourceWithRawResponse(self._client.tenants)
+
class ArkWithStreamedResponse:
_client: Ark
@@ -609,6 +634,12 @@ def usage(self) -> usage.UsageResourceWithStreamingResponse:
return UsageResourceWithStreamingResponse(self._client.usage)
+ @cached_property
+ def tenants(self) -> tenants.TenantsResourceWithStreamingResponse:
+ from .resources.tenants import TenantsResourceWithStreamingResponse
+
+ return TenantsResourceWithStreamingResponse(self._client.tenants)
+
class AsyncArkWithStreamedResponse:
_client: AsyncArk
@@ -658,6 +689,12 @@ def usage(self) -> usage.AsyncUsageResourceWithStreamingResponse:
return AsyncUsageResourceWithStreamingResponse(self._client.usage)
+ @cached_property
+ def tenants(self) -> tenants.AsyncTenantsResourceWithStreamingResponse:
+ from .resources.tenants import AsyncTenantsResourceWithStreamingResponse
+
+ return AsyncTenantsResourceWithStreamingResponse(self._client.tenants)
+
Client = Ark
diff --git a/src/ark/resources/__init__.py b/src/ark/resources/__init__.py
index 76b59c0..f3812e4 100644
--- a/src/ark/resources/__init__.py
+++ b/src/ark/resources/__init__.py
@@ -32,6 +32,14 @@
DomainsResourceWithStreamingResponse,
AsyncDomainsResourceWithStreamingResponse,
)
+from .tenants import (
+ TenantsResource,
+ AsyncTenantsResource,
+ TenantsResourceWithRawResponse,
+ AsyncTenantsResourceWithRawResponse,
+ TenantsResourceWithStreamingResponse,
+ AsyncTenantsResourceWithStreamingResponse,
+)
from .tracking import (
TrackingResource,
AsyncTrackingResource,
@@ -100,4 +108,10 @@
"AsyncUsageResourceWithRawResponse",
"UsageResourceWithStreamingResponse",
"AsyncUsageResourceWithStreamingResponse",
+ "TenantsResource",
+ "AsyncTenantsResource",
+ "TenantsResourceWithRawResponse",
+ "AsyncTenantsResourceWithRawResponse",
+ "TenantsResourceWithStreamingResponse",
+ "AsyncTenantsResourceWithStreamingResponse",
]
diff --git a/src/ark/resources/tenants.py b/src/ark/resources/tenants.py
new file mode 100644
index 0000000..e7b13a1
--- /dev/null
+++ b/src/ark/resources/tenants.py
@@ -0,0 +1,621 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict, Union, Optional
+from typing_extensions import Literal
+
+import httpx
+
+from ..types import tenant_list_params, tenant_create_params, tenant_update_params
+from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from .._utils import maybe_transform, async_maybe_transform
+from .._compat import cached_property
+from .._resource import SyncAPIResource, AsyncAPIResource
+from .._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..pagination import SyncPageNumberPagination, AsyncPageNumberPagination
+from .._base_client import AsyncPaginator, make_request_options
+from ..types.tenant import Tenant
+from ..types.tenant_create_response import TenantCreateResponse
+from ..types.tenant_delete_response import TenantDeleteResponse
+from ..types.tenant_update_response import TenantUpdateResponse
+from ..types.tenant_retrieve_response import TenantRetrieveResponse
+
+__all__ = ["TenantsResource", "AsyncTenantsResource"]
+
+
+class TenantsResource(SyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> TenantsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#accessing-raw-response-data-eg-headers
+ """
+ return TenantsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> TenantsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#with_streaming_response
+ """
+ return TenantsResourceWithStreamingResponse(self)
+
+ def create(
+ self,
+ *,
+ name: str,
+ metadata: Optional[Dict[str, Union[str, float, bool, None]]] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> TenantCreateResponse:
+ """Create a new tenant.
+
+ Returns the created tenant with a unique `id`.
+
+ Store this ID in your database to
+ reference this tenant later.
+
+ Args:
+ name: Display name for the tenant (e.g., your customer's company name)
+
+ metadata: Custom key-value pairs. Useful for storing references to your internal systems.
+
+ **Limits:**
+
+ - Max 50 keys
+ - Key names max 40 characters
+ - String values max 500 characters
+ - Total size max 8KB
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._post(
+ "/tenants",
+ body=maybe_transform(
+ {
+ "name": name,
+ "metadata": metadata,
+ },
+ tenant_create_params.TenantCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=TenantCreateResponse,
+ )
+
+ def retrieve(
+ self,
+ tenant_id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> TenantRetrieveResponse:
+ """
+ Get a tenant by ID.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return self._get(
+ f"/tenants/{tenant_id}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=TenantRetrieveResponse,
+ )
+
+ def update(
+ self,
+ tenant_id: str,
+ *,
+ metadata: Optional[Dict[str, Union[str, float, bool, None]]] | Omit = omit,
+ name: str | Omit = omit,
+ status: Literal["active", "suspended", "archived"] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> TenantUpdateResponse:
+ """Update a tenant's name, metadata, or status.
+
+ At least one field is required.
+
+ Metadata is replaced entirely—include all keys you want to keep.
+
+ Args:
+ metadata: Custom key-value pairs. Useful for storing references to your internal systems.
+
+ **Limits:**
+
+ - Max 50 keys
+ - Key names max 40 characters
+ - String values max 500 characters
+ - Total size max 8KB
+
+ name: Display name for the tenant
+
+ status: Tenant status
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return self._patch(
+ f"/tenants/{tenant_id}",
+ body=maybe_transform(
+ {
+ "metadata": metadata,
+ "name": name,
+ "status": status,
+ },
+ tenant_update_params.TenantUpdateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=TenantUpdateResponse,
+ )
+
+ def list(
+ self,
+ *,
+ page: int | Omit = omit,
+ per_page: int | Omit = omit,
+ status: Literal["active", "suspended", "archived"] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncPageNumberPagination[Tenant]:
+ """List all tenants with pagination.
+
+ Filter by `status` if needed.
+
+ Args:
+ page: Page number (1-indexed)
+
+ per_page: Number of items per page (max 100)
+
+ status: Filter by tenant status
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/tenants",
+ page=SyncPageNumberPagination[Tenant],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "page": page,
+ "per_page": per_page,
+ "status": status,
+ },
+ tenant_list_params.TenantListParams,
+ ),
+ ),
+ model=Tenant,
+ )
+
+ def delete(
+ self,
+ tenant_id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> TenantDeleteResponse:
+ """Permanently delete a tenant.
+
+ This cannot be undone.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return self._delete(
+ f"/tenants/{tenant_id}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=TenantDeleteResponse,
+ )
+
+
+class AsyncTenantsResource(AsyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> AsyncTenantsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#accessing-raw-response-data-eg-headers
+ """
+ return AsyncTenantsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncTenantsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#with_streaming_response
+ """
+ return AsyncTenantsResourceWithStreamingResponse(self)
+
+ async def create(
+ self,
+ *,
+ name: str,
+ metadata: Optional[Dict[str, Union[str, float, bool, None]]] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> TenantCreateResponse:
+ """Create a new tenant.
+
+ Returns the created tenant with a unique `id`.
+
+ Store this ID in your database to
+ reference this tenant later.
+
+ Args:
+ name: Display name for the tenant (e.g., your customer's company name)
+
+ metadata: Custom key-value pairs. Useful for storing references to your internal systems.
+
+ **Limits:**
+
+ - Max 50 keys
+ - Key names max 40 characters
+ - String values max 500 characters
+ - Total size max 8KB
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return await self._post(
+ "/tenants",
+ body=await async_maybe_transform(
+ {
+ "name": name,
+ "metadata": metadata,
+ },
+ tenant_create_params.TenantCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=TenantCreateResponse,
+ )
+
+ async def retrieve(
+ self,
+ tenant_id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> TenantRetrieveResponse:
+ """
+ Get a tenant by ID.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return await self._get(
+ f"/tenants/{tenant_id}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=TenantRetrieveResponse,
+ )
+
+ async def update(
+ self,
+ tenant_id: str,
+ *,
+ metadata: Optional[Dict[str, Union[str, float, bool, None]]] | Omit = omit,
+ name: str | Omit = omit,
+ status: Literal["active", "suspended", "archived"] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> TenantUpdateResponse:
+ """Update a tenant's name, metadata, or status.
+
+ At least one field is required.
+
+ Metadata is replaced entirely—include all keys you want to keep.
+
+ Args:
+ metadata: Custom key-value pairs. Useful for storing references to your internal systems.
+
+ **Limits:**
+
+ - Max 50 keys
+ - Key names max 40 characters
+ - String values max 500 characters
+ - Total size max 8KB
+
+ name: Display name for the tenant
+
+ status: Tenant status
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return await self._patch(
+ f"/tenants/{tenant_id}",
+ body=await async_maybe_transform(
+ {
+ "metadata": metadata,
+ "name": name,
+ "status": status,
+ },
+ tenant_update_params.TenantUpdateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=TenantUpdateResponse,
+ )
+
+ def list(
+ self,
+ *,
+ page: int | Omit = omit,
+ per_page: int | Omit = omit,
+ status: Literal["active", "suspended", "archived"] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[Tenant, AsyncPageNumberPagination[Tenant]]:
+ """List all tenants with pagination.
+
+ Filter by `status` if needed.
+
+ Args:
+ page: Page number (1-indexed)
+
+ per_page: Number of items per page (max 100)
+
+ status: Filter by tenant status
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/tenants",
+ page=AsyncPageNumberPagination[Tenant],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "page": page,
+ "per_page": per_page,
+ "status": status,
+ },
+ tenant_list_params.TenantListParams,
+ ),
+ ),
+ model=Tenant,
+ )
+
+ async def delete(
+ self,
+ tenant_id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> TenantDeleteResponse:
+ """Permanently delete a tenant.
+
+ This cannot be undone.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return await self._delete(
+ f"/tenants/{tenant_id}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=TenantDeleteResponse,
+ )
+
+
+class TenantsResourceWithRawResponse:
+ def __init__(self, tenants: TenantsResource) -> None:
+ self._tenants = tenants
+
+ self.create = to_raw_response_wrapper(
+ tenants.create,
+ )
+ self.retrieve = to_raw_response_wrapper(
+ tenants.retrieve,
+ )
+ self.update = to_raw_response_wrapper(
+ tenants.update,
+ )
+ self.list = to_raw_response_wrapper(
+ tenants.list,
+ )
+ self.delete = to_raw_response_wrapper(
+ tenants.delete,
+ )
+
+
+class AsyncTenantsResourceWithRawResponse:
+ def __init__(self, tenants: AsyncTenantsResource) -> None:
+ self._tenants = tenants
+
+ self.create = async_to_raw_response_wrapper(
+ tenants.create,
+ )
+ self.retrieve = async_to_raw_response_wrapper(
+ tenants.retrieve,
+ )
+ self.update = async_to_raw_response_wrapper(
+ tenants.update,
+ )
+ self.list = async_to_raw_response_wrapper(
+ tenants.list,
+ )
+ self.delete = async_to_raw_response_wrapper(
+ tenants.delete,
+ )
+
+
+class TenantsResourceWithStreamingResponse:
+ def __init__(self, tenants: TenantsResource) -> None:
+ self._tenants = tenants
+
+ self.create = to_streamed_response_wrapper(
+ tenants.create,
+ )
+ self.retrieve = to_streamed_response_wrapper(
+ tenants.retrieve,
+ )
+ self.update = to_streamed_response_wrapper(
+ tenants.update,
+ )
+ self.list = to_streamed_response_wrapper(
+ tenants.list,
+ )
+ self.delete = to_streamed_response_wrapper(
+ tenants.delete,
+ )
+
+
+class AsyncTenantsResourceWithStreamingResponse:
+ def __init__(self, tenants: AsyncTenantsResource) -> None:
+ self._tenants = tenants
+
+ self.create = async_to_streamed_response_wrapper(
+ tenants.create,
+ )
+ self.retrieve = async_to_streamed_response_wrapper(
+ tenants.retrieve,
+ )
+ self.update = async_to_streamed_response_wrapper(
+ tenants.update,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ tenants.list,
+ )
+ self.delete = async_to_streamed_response_wrapper(
+ tenants.delete,
+ )
diff --git a/src/ark/types/__init__.py b/src/ark/types/__init__.py
index 6cd75fb..23e9cb2 100644
--- a/src/ark/types/__init__.py
+++ b/src/ark/types/__init__.py
@@ -3,6 +3,7 @@
from __future__ import annotations
from .shared import APIMeta as APIMeta
+from .tenant import Tenant as Tenant
from .log_entry import LogEntry as LogEntry
from .dns_record import DNSRecord as DNSRecord
from .track_domain import TrackDomain as TrackDomain
@@ -10,12 +11,15 @@
from .log_entry_detail import LogEntryDetail as LogEntryDetail
from .email_list_params import EmailListParams as EmailListParams
from .email_send_params import EmailSendParams as EmailSendParams
+from .tenant_list_params import TenantListParams as TenantListParams
from .email_list_response import EmailListResponse as EmailListResponse
from .email_send_response import EmailSendResponse as EmailSendResponse
from .webhook_test_params import WebhookTestParams as WebhookTestParams
from .domain_create_params import DomainCreateParams as DomainCreateParams
from .domain_list_response import DomainListResponse as DomainListResponse
from .email_retry_response import EmailRetryResponse as EmailRetryResponse
+from .tenant_create_params import TenantCreateParams as TenantCreateParams
+from .tenant_update_params import TenantUpdateParams as TenantUpdateParams
from .email_retrieve_params import EmailRetrieveParams as EmailRetrieveParams
from .email_send_raw_params import EmailSendRawParams as EmailSendRawParams
from .log_retrieve_response import LogRetrieveResponse as LogRetrieveResponse
@@ -26,6 +30,9 @@
from .domain_create_response import DomainCreateResponse as DomainCreateResponse
from .domain_delete_response import DomainDeleteResponse as DomainDeleteResponse
from .domain_verify_response import DomainVerifyResponse as DomainVerifyResponse
+from .tenant_create_response import TenantCreateResponse as TenantCreateResponse
+from .tenant_delete_response import TenantDeleteResponse as TenantDeleteResponse
+from .tenant_update_response import TenantUpdateResponse as TenantUpdateResponse
from .tracking_create_params import TrackingCreateParams as TrackingCreateParams
from .tracking_list_response import TrackingListResponse as TrackingListResponse
from .tracking_update_params import TrackingUpdateParams as TrackingUpdateParams
@@ -38,6 +45,7 @@
from .webhook_delete_response import WebhookDeleteResponse as WebhookDeleteResponse
from .webhook_update_response import WebhookUpdateResponse as WebhookUpdateResponse
from .domain_retrieve_response import DomainRetrieveResponse as DomainRetrieveResponse
+from .tenant_retrieve_response import TenantRetrieveResponse as TenantRetrieveResponse
from .tracking_create_response import TrackingCreateResponse as TrackingCreateResponse
from .tracking_delete_response import TrackingDeleteResponse as TrackingDeleteResponse
from .tracking_update_response import TrackingUpdateResponse as TrackingUpdateResponse
diff --git a/src/ark/types/tenant.py b/src/ark/types/tenant.py
new file mode 100644
index 0000000..5c1591a
--- /dev/null
+++ b/src/ark/types/tenant.py
@@ -0,0 +1,34 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, Union
+from datetime import datetime
+from typing_extensions import Literal
+
+from .._models import BaseModel
+
+__all__ = ["Tenant"]
+
+
+class Tenant(BaseModel):
+ id: str
+ """Unique identifier for the tenant"""
+
+ created_at: datetime
+ """When the tenant was created"""
+
+ metadata: Dict[str, Union[str, float, bool, None]]
+ """Custom key-value pairs for storing additional data"""
+
+ name: str
+ """Display name for the tenant"""
+
+ status: Literal["active", "suspended", "archived"]
+ """Current status of the tenant:
+
+ - `active` - Normal operation
+ - `suspended` - Temporarily disabled
+ - `archived` - Soft-deleted
+ """
+
+ updated_at: datetime
+ """When the tenant was last updated"""
diff --git a/src/ark/types/tenant_create_params.py b/src/ark/types/tenant_create_params.py
new file mode 100644
index 0000000..9cac09c
--- /dev/null
+++ b/src/ark/types/tenant_create_params.py
@@ -0,0 +1,24 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict, Union, Optional
+from typing_extensions import Required, TypedDict
+
+__all__ = ["TenantCreateParams"]
+
+
+class TenantCreateParams(TypedDict, total=False):
+ name: Required[str]
+ """Display name for the tenant (e.g., your customer's company name)"""
+
+ metadata: Optional[Dict[str, Union[str, float, bool, None]]]
+ """Custom key-value pairs. Useful for storing references to your internal systems.
+
+ **Limits:**
+
+ - Max 50 keys
+ - Key names max 40 characters
+ - String values max 500 characters
+ - Total size max 8KB
+ """
diff --git a/src/ark/types/tenant_create_response.py b/src/ark/types/tenant_create_response.py
new file mode 100644
index 0000000..a44a7a8
--- /dev/null
+++ b/src/ark/types/tenant_create_response.py
@@ -0,0 +1,17 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from .tenant import Tenant
+from .._models import BaseModel
+from .shared.api_meta import APIMeta
+
+__all__ = ["TenantCreateResponse"]
+
+
+class TenantCreateResponse(BaseModel):
+ data: Tenant
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/src/ark/types/tenant_delete_response.py b/src/ark/types/tenant_delete_response.py
new file mode 100644
index 0000000..f7ba31a
--- /dev/null
+++ b/src/ark/types/tenant_delete_response.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from .._models import BaseModel
+from .shared.api_meta import APIMeta
+
+__all__ = ["TenantDeleteResponse", "Data"]
+
+
+class Data(BaseModel):
+ deleted: Literal[True]
+
+
+class TenantDeleteResponse(BaseModel):
+ data: Data
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/src/ark/types/tenant_list_params.py b/src/ark/types/tenant_list_params.py
new file mode 100644
index 0000000..0c083a9
--- /dev/null
+++ b/src/ark/types/tenant_list_params.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Annotated, TypedDict
+
+from .._utils import PropertyInfo
+
+__all__ = ["TenantListParams"]
+
+
+class TenantListParams(TypedDict, total=False):
+ page: int
+ """Page number (1-indexed)"""
+
+ per_page: Annotated[int, PropertyInfo(alias="perPage")]
+ """Number of items per page (max 100)"""
+
+ status: Literal["active", "suspended", "archived"]
+ """Filter by tenant status"""
diff --git a/src/ark/types/tenant_retrieve_response.py b/src/ark/types/tenant_retrieve_response.py
new file mode 100644
index 0000000..2b5f77d
--- /dev/null
+++ b/src/ark/types/tenant_retrieve_response.py
@@ -0,0 +1,17 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from .tenant import Tenant
+from .._models import BaseModel
+from .shared.api_meta import APIMeta
+
+__all__ = ["TenantRetrieveResponse"]
+
+
+class TenantRetrieveResponse(BaseModel):
+ data: Tenant
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/src/ark/types/tenant_update_params.py b/src/ark/types/tenant_update_params.py
new file mode 100644
index 0000000..651209c
--- /dev/null
+++ b/src/ark/types/tenant_update_params.py
@@ -0,0 +1,27 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict, Union, Optional
+from typing_extensions import Literal, TypedDict
+
+__all__ = ["TenantUpdateParams"]
+
+
+class TenantUpdateParams(TypedDict, total=False):
+ metadata: Optional[Dict[str, Union[str, float, bool, None]]]
+ """Custom key-value pairs. Useful for storing references to your internal systems.
+
+ **Limits:**
+
+ - Max 50 keys
+ - Key names max 40 characters
+ - String values max 500 characters
+ - Total size max 8KB
+ """
+
+ name: str
+ """Display name for the tenant"""
+
+ status: Literal["active", "suspended", "archived"]
+ """Tenant status"""
diff --git a/src/ark/types/tenant_update_response.py b/src/ark/types/tenant_update_response.py
new file mode 100644
index 0000000..8fa7d3e
--- /dev/null
+++ b/src/ark/types/tenant_update_response.py
@@ -0,0 +1,17 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from .tenant import Tenant
+from .._models import BaseModel
+from .shared.api_meta import APIMeta
+
+__all__ = ["TenantUpdateResponse"]
+
+
+class TenantUpdateResponse(BaseModel):
+ data: Tenant
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/tests/api_resources/test_tenants.py b/tests/api_resources/test_tenants.py
new file mode 100644
index 0000000..0f02dd2
--- /dev/null
+++ b/tests/api_resources/test_tenants.py
@@ -0,0 +1,441 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from ark import Ark, AsyncArk
+from ark.types import (
+ Tenant,
+ TenantCreateResponse,
+ TenantDeleteResponse,
+ TenantUpdateResponse,
+ TenantRetrieveResponse,
+)
+from tests.utils import assert_matches_type
+from ark.pagination import SyncPageNumberPagination, AsyncPageNumberPagination
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestTenants:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @parametrize
+ def test_method_create(self, client: Ark) -> None:
+ tenant = client.tenants.create(
+ name="Acme Corp",
+ )
+ assert_matches_type(TenantCreateResponse, tenant, path=["response"])
+
+ @parametrize
+ def test_method_create_with_all_params(self, client: Ark) -> None:
+ tenant = client.tenants.create(
+ name="Acme Corp",
+ metadata={
+ "plan": "pro",
+ "internal_id": "cust_12345",
+ "region": "us-west",
+ },
+ )
+ assert_matches_type(TenantCreateResponse, tenant, path=["response"])
+
+ @parametrize
+ def test_raw_response_create(self, client: Ark) -> None:
+ response = client.tenants.with_raw_response.create(
+ name="Acme Corp",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ tenant = response.parse()
+ assert_matches_type(TenantCreateResponse, tenant, path=["response"])
+
+ @parametrize
+ def test_streaming_response_create(self, client: Ark) -> None:
+ with client.tenants.with_streaming_response.create(
+ name="Acme Corp",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ tenant = response.parse()
+ assert_matches_type(TenantCreateResponse, tenant, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_method_retrieve(self, client: Ark) -> None:
+ tenant = client.tenants.retrieve(
+ "cm6abc123def456",
+ )
+ assert_matches_type(TenantRetrieveResponse, tenant, path=["response"])
+
+ @parametrize
+ def test_raw_response_retrieve(self, client: Ark) -> None:
+ response = client.tenants.with_raw_response.retrieve(
+ "cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ tenant = response.parse()
+ assert_matches_type(TenantRetrieveResponse, tenant, path=["response"])
+
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Ark) -> None:
+ with client.tenants.with_streaming_response.retrieve(
+ "cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ tenant = response.parse()
+ assert_matches_type(TenantRetrieveResponse, tenant, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_retrieve(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.with_raw_response.retrieve(
+ "",
+ )
+
+ @parametrize
+ def test_method_update(self, client: Ark) -> None:
+ tenant = client.tenants.update(
+ tenant_id="cm6abc123def456",
+ )
+ assert_matches_type(TenantUpdateResponse, tenant, path=["response"])
+
+ @parametrize
+ def test_method_update_with_all_params(self, client: Ark) -> None:
+ tenant = client.tenants.update(
+ tenant_id="cm6abc123def456",
+ metadata={
+ "plan": "pro",
+ "internal_id": "cust_12345",
+ "region": "us-west",
+ },
+ name="Acme Corporation",
+ status="active",
+ )
+ assert_matches_type(TenantUpdateResponse, tenant, path=["response"])
+
+ @parametrize
+ def test_raw_response_update(self, client: Ark) -> None:
+ response = client.tenants.with_raw_response.update(
+ tenant_id="cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ tenant = response.parse()
+ assert_matches_type(TenantUpdateResponse, tenant, path=["response"])
+
+ @parametrize
+ def test_streaming_response_update(self, client: Ark) -> None:
+ with client.tenants.with_streaming_response.update(
+ tenant_id="cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ tenant = response.parse()
+ assert_matches_type(TenantUpdateResponse, tenant, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_update(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.with_raw_response.update(
+ tenant_id="",
+ )
+
+ @parametrize
+ def test_method_list(self, client: Ark) -> None:
+ tenant = client.tenants.list()
+ assert_matches_type(SyncPageNumberPagination[Tenant], tenant, path=["response"])
+
+ @parametrize
+ def test_method_list_with_all_params(self, client: Ark) -> None:
+ tenant = client.tenants.list(
+ page=1,
+ per_page=1,
+ status="active",
+ )
+ assert_matches_type(SyncPageNumberPagination[Tenant], tenant, path=["response"])
+
+ @parametrize
+ def test_raw_response_list(self, client: Ark) -> None:
+ response = client.tenants.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ tenant = response.parse()
+ assert_matches_type(SyncPageNumberPagination[Tenant], tenant, path=["response"])
+
+ @parametrize
+ def test_streaming_response_list(self, client: Ark) -> None:
+ with client.tenants.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ tenant = response.parse()
+ assert_matches_type(SyncPageNumberPagination[Tenant], tenant, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_method_delete(self, client: Ark) -> None:
+ tenant = client.tenants.delete(
+ "cm6abc123def456",
+ )
+ assert_matches_type(TenantDeleteResponse, tenant, path=["response"])
+
+ @parametrize
+ def test_raw_response_delete(self, client: Ark) -> None:
+ response = client.tenants.with_raw_response.delete(
+ "cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ tenant = response.parse()
+ assert_matches_type(TenantDeleteResponse, tenant, path=["response"])
+
+ @parametrize
+ def test_streaming_response_delete(self, client: Ark) -> None:
+ with client.tenants.with_streaming_response.delete(
+ "cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ tenant = response.parse()
+ assert_matches_type(TenantDeleteResponse, tenant, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_delete(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.with_raw_response.delete(
+ "",
+ )
+
+
+class TestAsyncTenants:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @parametrize
+ async def test_method_create(self, async_client: AsyncArk) -> None:
+ tenant = await async_client.tenants.create(
+ name="Acme Corp",
+ )
+ assert_matches_type(TenantCreateResponse, tenant, path=["response"])
+
+ @parametrize
+ async def test_method_create_with_all_params(self, async_client: AsyncArk) -> None:
+ tenant = await async_client.tenants.create(
+ name="Acme Corp",
+ metadata={
+ "plan": "pro",
+ "internal_id": "cust_12345",
+ "region": "us-west",
+ },
+ )
+ assert_matches_type(TenantCreateResponse, tenant, path=["response"])
+
+ @parametrize
+ async def test_raw_response_create(self, async_client: AsyncArk) -> None:
+ response = await async_client.tenants.with_raw_response.create(
+ name="Acme Corp",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ tenant = await response.parse()
+ assert_matches_type(TenantCreateResponse, tenant, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_create(self, async_client: AsyncArk) -> None:
+ async with async_client.tenants.with_streaming_response.create(
+ name="Acme Corp",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ tenant = await response.parse()
+ assert_matches_type(TenantCreateResponse, tenant, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncArk) -> None:
+ tenant = await async_client.tenants.retrieve(
+ "cm6abc123def456",
+ )
+ assert_matches_type(TenantRetrieveResponse, tenant, path=["response"])
+
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncArk) -> None:
+ response = await async_client.tenants.with_raw_response.retrieve(
+ "cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ tenant = await response.parse()
+ assert_matches_type(TenantRetrieveResponse, tenant, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncArk) -> None:
+ async with async_client.tenants.with_streaming_response.retrieve(
+ "cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ tenant = await response.parse()
+ assert_matches_type(TenantRetrieveResponse, tenant, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_retrieve(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.with_raw_response.retrieve(
+ "",
+ )
+
+ @parametrize
+ async def test_method_update(self, async_client: AsyncArk) -> None:
+ tenant = await async_client.tenants.update(
+ tenant_id="cm6abc123def456",
+ )
+ assert_matches_type(TenantUpdateResponse, tenant, path=["response"])
+
+ @parametrize
+ async def test_method_update_with_all_params(self, async_client: AsyncArk) -> None:
+ tenant = await async_client.tenants.update(
+ tenant_id="cm6abc123def456",
+ metadata={
+ "plan": "pro",
+ "internal_id": "cust_12345",
+ "region": "us-west",
+ },
+ name="Acme Corporation",
+ status="active",
+ )
+ assert_matches_type(TenantUpdateResponse, tenant, path=["response"])
+
+ @parametrize
+ async def test_raw_response_update(self, async_client: AsyncArk) -> None:
+ response = await async_client.tenants.with_raw_response.update(
+ tenant_id="cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ tenant = await response.parse()
+ assert_matches_type(TenantUpdateResponse, tenant, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_update(self, async_client: AsyncArk) -> None:
+ async with async_client.tenants.with_streaming_response.update(
+ tenant_id="cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ tenant = await response.parse()
+ assert_matches_type(TenantUpdateResponse, tenant, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_update(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.with_raw_response.update(
+ tenant_id="",
+ )
+
+ @parametrize
+ async def test_method_list(self, async_client: AsyncArk) -> None:
+ tenant = await async_client.tenants.list()
+ assert_matches_type(AsyncPageNumberPagination[Tenant], tenant, path=["response"])
+
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncArk) -> None:
+ tenant = await async_client.tenants.list(
+ page=1,
+ per_page=1,
+ status="active",
+ )
+ assert_matches_type(AsyncPageNumberPagination[Tenant], tenant, path=["response"])
+
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncArk) -> None:
+ response = await async_client.tenants.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ tenant = await response.parse()
+ assert_matches_type(AsyncPageNumberPagination[Tenant], tenant, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncArk) -> None:
+ async with async_client.tenants.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ tenant = await response.parse()
+ assert_matches_type(AsyncPageNumberPagination[Tenant], tenant, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_method_delete(self, async_client: AsyncArk) -> None:
+ tenant = await async_client.tenants.delete(
+ "cm6abc123def456",
+ )
+ assert_matches_type(TenantDeleteResponse, tenant, path=["response"])
+
+ @parametrize
+ async def test_raw_response_delete(self, async_client: AsyncArk) -> None:
+ response = await async_client.tenants.with_raw_response.delete(
+ "cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ tenant = await response.parse()
+ assert_matches_type(TenantDeleteResponse, tenant, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_delete(self, async_client: AsyncArk) -> None:
+ async with async_client.tenants.with_streaming_response.delete(
+ "cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ tenant = await response.parse()
+ assert_matches_type(TenantDeleteResponse, tenant, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_delete(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.with_raw_response.delete(
+ "",
+ )
From c91786e0e12c49466e9b85b1860e3abbd9b79c0a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 3 Feb 2026 10:30:50 +0000
Subject: [PATCH 6/6] release: 0.17.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 12 ++++++++++++
pyproject.toml | 2 +-
src/ark/_version.py | 2 +-
4 files changed, 15 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index b4e9013..6db19b9 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.16.0"
+ ".": "0.17.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b2c54d3..5a605d7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# Changelog
+## 0.17.0 (2026-02-03)
+
+Full Changelog: [v0.16.0...v0.17.0](https://github.com/ArkHQ-io/ark-python/compare/v0.16.0...v0.17.0)
+
+### Features
+
+* **api:** Add Tenants ([8ba85ac](https://github.com/ArkHQ-io/ark-python/commit/8ba85ac06c8e1d803f2cf5077a1ff99d4655e178))
+* **api:** api update ([eed2900](https://github.com/ArkHQ-io/ark-python/commit/eed2900c69e717e346fe6d5d10d95be29771e233))
+* **api:** manual updates ([f355920](https://github.com/ArkHQ-io/ark-python/commit/f355920e2008cf0d5990b15c7292483279ee671c))
+* **api:** manual updates ([0e5c6fe](https://github.com/ArkHQ-io/ark-python/commit/0e5c6fe1479ca123629e23c842fc3cdf231876e5))
+* **api:** manual updates ([e310cbd](https://github.com/ArkHQ-io/ark-python/commit/e310cbdd0ce739190f1094b6ea1c5dd539eb91c4))
+
## 0.16.0 (2026-01-30)
Full Changelog: [v0.15.0...v0.16.0](https://github.com/ArkHQ-io/ark-python/compare/v0.15.0...v0.16.0)
diff --git a/pyproject.toml b/pyproject.toml
index c95a3cf..1fc344a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "ark-email"
-version = "0.16.0"
+version = "0.17.0"
description = "The official Python library for the ark API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/ark/_version.py b/src/ark/_version.py
index 1f10948..8bd9d9e 100644
--- a/src/ark/_version.py
+++ b/src/ark/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "ark"
-__version__ = "0.16.0" # x-release-please-version
+__version__ = "0.17.0" # x-release-please-version