This guide covers upgrading from seam v2.x to v3 of the Seam Python SDK.
Version 3 replaces the underlying HTTP library, adds client-side validation and explicit null support, and regenerates the API surface against the latest Seam API. Most application code — authentication, method names, resource models, action attempts, and pagination — works unchanged. The breaking changes are concentrated in client configuration and error handling.
pip install --upgrade 'seam>=3,<4'| Change | Affects you if... |
|---|---|
| Python 3.11+ required | You run Python 3.10 |
| httpx replaces niquests | You pass niquests_options, catch niquests exceptions, or touch seam.client directly |
retries takes an httpx_retries.Retry |
You pass a custom retries option |
| Endpoints validate parameters client-side | You call endpoints with no parameters, or rely on the server's 400 response |
lts_version removed |
You read Seam.lts_version or the seam-lts-version header |
| Preferred HTTP methods and URL search params | You inspect traffic in a proxy, mock server, or firewall rules |
Version 2 supported Python 3.10. Version 3 requires Python >= 3.11 and is tested on Python 3.11 through 3.14.
The SDK's HTTP layer is now httpx instead of niquests. This surfaces in three places.
Options are now passed to the underlying httpx.Client, so both the option name and its contents change. For example, connection pool limits:
# v2
seam = Seam(
api_key="your-api-key",
niquests_options={"pool_connections": 20, "pool_maxsize": 25},
)
# v3
from httpx import Limits
seam = Seam(
api_key="your-api-key",
httpx_options={
"limits": Limits(max_connections=25, max_keepalive_connections=20),
},
)This applies to Seam(), Seam.from_api_key(), Seam.from_personal_access_token(), and SeamWithoutWorkspace.
Requests that time out now raise httpx.TimeoutException instead of niquests.exceptions.Timeout, and connection failures raise httpx transport errors (httpx.ConnectError, etc.) instead of niquests/urllib3 ones.
# v2
import niquests
try:
seam.devices.list()
except niquests.exceptions.Timeout:
...
# v3
import httpx
try:
seam.devices.list()
except httpx.TimeoutException:
...Seam API errors are unchanged: SeamHttpApiError, SeamHttpInvalidInputError, and SeamHttpUnauthorizedError are raised exactly as in v2.
If you access the client directly, it is now an httpx.Client subclass rather than a niquests Session. Notably, response hooks are registered via event_hooks instead of hooks.
The retries option now takes a Retry object from httpx-retries instead of urllib3.util.retry.Retry. The class is re-exported from seam for convenience:
# v2
from urllib3.util.retry import Retry
seam = Seam(api_key="your-api-key", retries=Retry(total=3))
# v3
from seam import Seam, Retry
seam = Seam(
api_key="your-api-key",
retries=Retry(total=3, backoff_factor=0.5, status_forcelist=[503]),
)The default retry policy is now explicit and documented. Out of the box, the SDK makes up to three attempts: the initial request and two retries. Retries are limited to GET, HEAD, OPTIONS, PUT, and DELETE requests that fail because of a transport error, timeout, HTTP 429 response, or HTTP 5xx response. POST and PATCH requests are never retried. Retries use exponential backoff with jitter, and a Retry-After header is honored instead of the calculated backoff.
In v2, the default was urllib3's implicit Retry() (connection-level retries only, with no retries on HTTP status codes such as 429 or 5xx). If you depended on requests never being retried on 429/5xx, pass an explicit policy, e.g. retries=Retry(total=0).
Endpoints that require at least one parameter now raise ValueError locally instead of sending the request and letting the server reject it:
# v2: raises SeamHttpInvalidInputError after a round trip to the server
# v3: raises ValueError("At least one parameter is required for /locks/get")
seam.locks.get()create_paginator is validated the same way. It raises ValueError when given a non-paginated endpoint, and when given an endpoint that requires parameters without any:
# v3: raises ValueError - /devices/get is not paginated
seam.create_paginator(seam.devices.get)If you catch SeamHttpInvalidInputError around calls that could be sent with no parameters, also handle ValueError (or fix the call site).
The Seam.lts_version / SeamWithoutWorkspace.lts_version attribute and the seam-lts-version request header no longer exist. There is no replacement; use the package version instead:
from importlib.metadata import version
version("seam")In v2, every endpoint was called with POST and a JSON body. In v3, endpoints use the HTTP method the Seam API prefers:
- Read endpoints (
get,list, and friends) useGET, with parameters sent as URL search params serialized per Seam's URL search params standard. - Update endpoints use
PATCHorPUT. - Delete endpoints use
DELETE. - Create and action endpoints (
create,lock_door, etc.) remainPOST.
Method signatures, arguments, and return values are unchanged — this only matters if something outside your code observes the HTTP traffic: proxy or firewall rules that allowlist methods, request logging, or test mocks registered against POST routes. Note the interaction with the new retry defaults: because reads are now GET, they are retried by default, which they were not in v2 (as POST).
If you call the Seam API with your own HTTP client, the serializer used for GET params is exported:
import httpx
from seam import serialize_url_search_params
httpx.get(
"https://connect.getseam.com/devices/list",
params=serialize_url_search_params({"device_ids": ["device1", "device2"]}),
headers={"Authorization": "Bearer your-api-key"},
)These are additions, not breaking changes, but they are worth adopting while you migrate.
The Seam API distinguishes an omitted parameter from one explicitly set to null: in an update request, an omitted parameter leaves the current value unchanged, while a null parameter unsets it. Version 2 had no way to send null — None always meant "omit". Version 3 keeps that behavior for None and adds a NULL sentinel for sending an explicit null:
from seam import NULL, Seam
seam = Seam()
# Leaves the name unchanged (same as v2).
seam.devices.update(device_id="your-device-id", name=None)
# Unsets the name (new in v3).
seam.devices.update(device_id="your-device-id", name=NULL)Only parameters the Seam API documents as nullable are typed to accept NULL, so a type checker will flag misuse. The sentinel's type is exported as Null for annotating your own code.
seam now exports NULL, Null, Retry (from httpx-retries), UrlSearchParams, serialize_url_search_params, update_url_search_params, and UnserializableParamError, alongside everything exported in v2.
- Upgrade your runtime to Python 3.11 or later.
- Update the dependency:
seam>=3,<4. - Rename
niquests_optionstohttpx_optionsand translate its contents tohttpx.Clientoptions. - Replace
urllib3.util.retry.Retrywithseam.Retry(httpx-retries) in anyretriesargument, and review the new default retry policy. - Replace handling of
niquests/urllib3exceptions with thehttpxequivalents (httpx.TimeoutException,httpx.ConnectError, ...). Seam error classes are unchanged. - Remove any use of
lts_versionor theseam-lts-versionheader. - Handle
ValueErrorfrom endpoints andcreate_paginatorwhere calls might carry no parameters. - If proxies, firewalls, or test mocks assume all requests are
POST, update them forGET/PATCH/PUT/DELETE. - Optionally, adopt
NULLwhere you need to unset nullable values.
If you are still on v1.x, migrate to v2 first (or apply both guides together). Version 2 is a much smaller upgrade than v3: client configuration, authentication, endpoint methods, and error handling are all unchanged. The breaking changes are in resource objects and one class rename.
| Change | Affects you if... |
|---|---|
| Nested resource properties are typed objects | You treat nested properties as dicts, or rely on unknown-attribute reads |
SeamMultiWorkspace renamed to SeamWithoutWorkspace |
You use SeamMultiWorkspace |
In v1, nested properties on resources — for example device.properties or action_attempt.result — were dict subclasses with attribute access layered on top. In v2, they hydrate as typed dataclasses scoped to their parent resource, such as Device.Properties and ActionAttempt.Result, so IDEs and type checkers can see their fields.
Attribute access and dictionary-style reads keep working:
device = seam.devices.get(device_id="your-device-id")
device.properties.locked # still works
device.properties["locked"] # still works
device.properties.get("online") # still works
"locked" in device.properties # still worksWhat breaks:
- They are no longer dicts.
isinstance(device.properties, dict)is nowFalse, and mutation (device.properties["x"] = ...) and dict-only methods such as.items()and.values()are gone. Iterate over.keys()and index instead. - Typoed attributes raise
AttributeError. In v1, reading an unknown attribute silently returned (and inserted) an empty mapping, so typos went unnoticed and were truthy-checked as empty dicts. In v2 they fail loudly — code that probed for optional fields via bare attribute access should use.get("field")orhasattr. - Undocumented nested fields are stripped. API fields not (yet) in the SDK's generated types are dropped during hydration instead of being passed through. If you depend on a field the SDK does not model, upgrade the SDK to a version that includes it.
Free-form record properties, such as custom_metadata, remain plain mappings and are not affected.
The client for personal access tokens without a workspace is renamed; there is no compatibility alias. Its constructor, options, and methods are otherwise identical:
# v1
from seam import SeamMultiWorkspace
seam = SeamMultiWorkspace(personal_access_token="your-personal-access-token")
# v2
from seam import SeamWithoutWorkspace
seam = SeamWithoutWorkspace(personal_access_token="your-personal-access-token")The abstract base class is likewise renamed from AbstractSeamMultiWorkspace to AbstractSeamWithoutWorkspace.
Version 2.2 also reads authentication from the environment: SEAM_PERSONAL_ACCESS_TOKEN and SEAM_WORKSPACE_ID are picked up when no explicit credentials are passed (SEAM_API_KEY was already supported in v1). Setting both SEAM_API_KEY and SEAM_PERSONAL_ACCESS_TOKEN is an error.
- Update the dependency:
seam>=2,<3. - Rename
SeamMultiWorkspacetoSeamWithoutWorkspace(andAbstractSeamMultiWorkspacetoAbstractSeamWithoutWorkspace). - Replace dict-style mutation and
.items()/.values()/isinstance(..., dict)usage on nested resource properties with attribute access or.keys()iteration. - Replace bare attribute probes for optional nested fields with
.get()orhasattr— unknown attributes now raiseAttributeError.