Skip to main content

seventhings Python SDK

PyPI CI

Python client for the seventhings Customer API (/customer-api/v1). It offers the same features as the Go and PHP SDKs (v1.4.0) and comes with both a synchronous and an asynchronous client.

  • Python 3.10+
  • The only runtime dependency is httpx
  • Fully typed (py.typed), with dataclass models

Installation

pip install seventhings-customer-api
# or
uv add seventhings-customer-api

The import name is seventhings.

Quick Start

Password authentication

from seventhings import Client

with Client.with_credentials(
    "https://example.seventhings.com", "user@example.com", "password", "client-id"
) as client:
    print(client.objects.count())

Pre-existing token

client = Client("https://example.seventhings.com", token="my-jwt-token")

Manual login and refresh

client = Client("https://example.seventhings.com")
tok = client.auth.login("user@example.com", "password", "client-id")  # stores the access token
tok = client.auth.refresh(tok.refresh_token)  # reuses the client ID from login
client.auth.revoke_tokens()

SSO authentication

from seventhings.models import SSOAppTarget, SSOProviderName

tok = client.auth.login_sso(SSOProviderName.AZURE, auth_code, "client-id", SSOAppTarget.WEB)

Async

AsyncClient has the same API as Client, except that its methods are coroutines, the all() iterators are async iterators, and you close it with aclose() or async with:

from seventhings import AsyncClient

async with await AsyncClient.with_credentials(url, user, password, client_id) as client:
    obj = await client.objects.get(uuid)
    async for room in client.rooms.all():
        print(room.name)

Configuration

Client(
    base_url,  # instance URL; "/customer-api/v1" is appended
    token=None,  # bearer token
    client_id=None,  # OAuth client ID used by auth.refresh()
    http_client=None,  # your own httpx.Client / httpx.AsyncClient (not closed by the SDK)
    timeout=30.0,  # seconds; None disables the timeout
)

Usage

Ping

ping = client.ping()  # unauthenticated
print(ping.status)  # "OK"

Objects

Objects have a schema that differs per tenant, so they are plain dicts. The all() iterator yields Fields, a dict with typed accessors.

from seventhings.models import ListOptions, SortDirection, like

uuid = client.objects.create({"inventory_name": "Laptop", "barcode": "INV-001"})
obj = client.objects.get(uuid)
obj = client.objects.get_by_barcode("INV-001")  # archived objects included
client.objects.patch(uuid, {"inventory_name": "Laptop (IT)"})
client.objects.archive(uuid)
client.objects.unarchive(uuid)
client.objects.delete(uuid)

page = client.objects.list(ListOptions(page=1, per_page=50).where(like("inventory_name", "Laptop")))
total = client.objects.count()

for obj in client.objects.all(ListOptions(per_page=100)):  # walks every page
    print(obj.uuid, obj.get_str("inventory_name"), obj.get_time("updated_at"))

To attach a file you have already uploaded to an attachment field, use add_files. It returns the raw Response, because the API can answer 207 Multi-Status:

from seventhings.models import FileAttachment

resp = client.objects.add_files(uuid, [FileAttachment("documents", file_uuid)])
client.objects.remove_files(uuid, [FileAttachment("documents", file_uuid)])

History

Objects, rooms, locations, persons, tasks and rental cases all have a paged history. The API defaults to page 1 with 50 entries per page (maximum 200).

from seventhings.models import HistoryListOptions

hist = client.persons.history(person_uuid, HistoryListOptions(page=1, per_page=20))
for entry in hist.items:
    print(entry.occurred_at, entry.event_name, entry.details)  # details is a JSON string
if hist.page * hist.per_page < hist.total:
    ...  # fetch the next page

Object history entries are plain dicts, because their shape depends on type (asset, task, rental_case or object_merge).

PDF reports

from seventhings.models import CreateReport

templates = client.reports.list_templates()
pdf: bytes = client.reports.create(CreateReport(templates[0].uuid, [obj_uuid]))

Files

with open("photo.jpg", "rb") as fh:
    file_uuid = client.files.upload("photo.jpg", fh)  # or pass bytes
meta = client.files.get(file_uuid)
data = client.files.get_data(file_uuid)
thumb = client.files.get_thumbnail(file_uuid)
recent = client.files.list()  # the most recent files (max 20)

Tasks

from seventhings.models import (
    CreateTask,
    TaskListOptions,
    TaskReferenceInput,
    TaskStatus,
    TimeInterval,
    TimeIntervalUnit,
    UpdateTask,
)

task_uuid = client.tasks.create(
    CreateTask(
        title="Inspect",
        deadline="2026-12-31",
        assignees=[user_uuid],
        references=[TaskReferenceInput(obj_uuid)],
        reminders=[TimeInterval(TimeIntervalUnit.DAYS, 1)],
    )
)
tasks = client.tasks.list(TaskListOptions(status=TaskStatus.OPEN))
client.tasks.update_status(task_uuid, TaskStatus.CLOSED)
client.tasks.update(task_uuid, UpdateTask(title="Inspect again", assignees=[user_uuid]))  # PUT
client.tasks.delete(task_uuid)

Rental cases

from seventhings.models import (
    CreateRentalCase,
    RentalCaseReferenceInput,
    RentalCaseRenter,
    RenterType,
)

rental_uuid = client.rentals.create(
    CreateRentalCase(
        title="Laptop loan",
        renter=RentalCaseRenter(RenterType.USER, user_uuid),
        references=[RentalCaseReferenceInput(obj_uuid)],
        issue_date="2026-01-01 09:00:00",
        due_date="2026-01-08 09:00:00",
        responsible_user_uuid=user_uuid,
    )
)
rental = client.rentals.get(rental_uuid)
for rc in client.rentals.all():
    print(rc.title, rc.status)

Rooms and locations

These work like objects. The difference is that patch returns the updated record, and responses that come back as a {uuid, fields} envelope are flattened into a single dict for you.

loc_uuid = client.locations.create({"name": "HQ"})
room_uuid = client.rooms.create({"name": "Office 1", "building_id": 1})
room = client.rooms.patch(room_uuid, {"name": "Office 1a"})

Users and persons

from seventhings.models import PersonListOptions, UserListOptions, UserSortBy, UserSortOrder

users = client.users.list(UserListOptions(sort_by=UserSortBy.EMAIL, order=UserSortOrder.ASC))
me = client.users.get_by_id(tok.user_id)

person_uuid = client.persons.create({"email": "ada@example.com", "first_name": "Ada"})
person = client.persons.get(person_uuid)
print(person.email, person.fields.get_str("cost_center"))  # every field is in person.fields
client.persons.patch(person_uuid, {"department": "IT"})
for p in client.persons.all(PersonListOptions(sort_by="last_name")):
    ...

Field definitions

from seventhings.models import AssetTrackingTemplate

defs = client.field_definitions.list(AssetTrackingTemplate.ASSET)
required = client.field_definitions.mandatory(AssetTrackingTemplate.ASSET)
missing = client.field_definitions.missing_mandatory_fields(AssetTrackingTemplate.ASSET, payload)
options = defs[0].field_type.allowed_values()  # dropdown values

Circularity Hub

Items and orders in the Circularity Hub use integer IDs.

from seventhings.models import AddObjectEntry, FilterObject, FilterOperator

suggestions = client.circularity_hub.suggest_category(
    FilterObject(filter={"uuid": {FilterOperator.IN: [obj_uuid]}})
)  # None when there are no suggestions
client.circularity_hub.add_objects({obj_uuid: AddObjectEntry("chairs", "25.00")})
for item in client.circularity_hub.all_items():
    ...
order_id = client.circularity_hub.create_order([1, 2])
order = client.circularity_hub.get_order(order_id)

Raw requests

request() covers anything the SDK doesn't wrap:

resp = client.request("GET", "objects", query="page=1&per_page=5")
print(resp.status_code, resp.json())

Filtering and sorting

from seventhings.models import ListOptions, SortDirection, gte, in_, like

opts = (
    ListOptions(page=1, per_page=50)
    .sort_by("name", SortDirection.ASC)
    .sort_by("created_at", SortDirection.DESC)
    .where(like("name", "Laptop"), in_("status", "active", "pending"), gte("price", "100"))
)

This produces the following query string (brackets are sent literally and only the values are escaped):

page=1&per_page=50&sort[name]=ASC&sort[created_at]=DESC&filter[name][like][]=Laptop&filter[status][in][]=active&filter[status][in][]=pending&filter[price][gte]=100
Operator Helper Description
eq eq Equal
neq neq Not equal
gt, gte gt, gte Greater than (or equal)
gt_or_null, gte_or_null gt_or_null, gte_or_null Greater than (or equal), including null
lt, lte lt, lte Less than (or equal)
lt_or_null, lte_or_null lt_or_null, lte_or_null Less than (or equal), including null
like like Contains substring (multi-value)
not_like not_like Does not contain substring (multi-value)
in in_ Value in set (multi-value)
nin nin Value not in set (multi-value)

Error handling

Every exception derives from seventhings.SeventhingsError:

Exception When
APIError The API returned a status of 400 or higher
NetworkError Connection failure, timeout or another transport error (wraps the httpx exception)
DecodeError The response could not be decoded, e.g. invalid JSON or a missing Location header
from seventhings import APIError

try:
    client.objects.get("nonexistent")
except APIError as err:
    print(err.status_code, err.body)
    if err.is_not_found:  # also: is_unauthorized, is_forbidden, is_conflict,
        ...  # is_rate_limited, is_server_error, is_feature_inactive

is_feature_inactive is true when the endpoint's module (for example rentals) is not active on the instance.

Pagination

list() methods fetch a single page. The all() iterators on objects, rooms, locations, rentals, users, persons and Circularity Hub items (all_items()) walk through every page:

  • they ignore opts.page;
  • they use opts.per_page as the page size, defaulting to 100;
  • they stop at the first page that is shorter than the page size.

Tasks and files have no paging. History is paged manually.

Scope and limitations

  • No automatic token refresh. Call client.auth.refresh(refresh_token) yourself when the access token expires.
  • No automatic retry and no rate limiting. Wrap calls yourself if you need either, or pass an httpx client that uses a retrying transport.
  • Dates are strings, as the API sends them (Y-m-d H:i:s, UTC). Fields.get_time() parses them.
  • Unknown enum values from newer API versions are kept as plain strings rather than raising an error.

Development

uv sync
uv run pytest                       # unit tests; each one runs against both Client and AsyncClient
uv run ruff check . && uv run ruff format --check .
uv run mypy                         # strict
uv run python scripts/unasync.py    # regenerate the sync client after editing _async/client.py

The synchronous client (src/seventhings/_sync/client.py) is generated from src/seventhings/_async/client.py, so edit the async file and regenerate. CI runs scripts/unasync.py --check. Request building and response decoding live in src/seventhings/_operations.py, which does no I/O and is shared by both clients.

Integration tests

The integration tests run against a live instance and create, then delete, their own test data:

cp .env.example .env   # fill in SEVENTHINGS_BASE_URL/USERNAME/PASSWORD/CLIENT_ID
scripts/run-integration.sh            # or: uv run pytest -m integration
scripts/run-integration.sh -k person  # forward pytest args

examples/demo/ walks through the SDK end to end and uses the same environment variables.

License

MIT

Release files for seventhings-customer-api 1.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for seventhings-customer-api 1.4.0
File Size Uploaded
seventhings_customer_api-1.4.0.tar.gz 93.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for seventhings-customer-api 1.4.0
File Interpreter ABI Platform
seventhings_customer_api-1.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 133.5 kB

Release files / seventhings_customer_api-1.4.0.tar.gz

Download URL seventhings_customer_api-1.4.0.tar.gz
Size 93.0 kB
Tags Source
SHA-256 checksum
How to use checksums
ee6fce732fa8c5a9d0780708bbd2142df444e60ef9994ddc6aff28e5e1f2feb7
BLAKE2b-256 checksum
How to use checksums
1724f9fa2dc6c23616cc2bcbbdfe39f026e24686393e40334016588a871c5eac
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / seventhings_customer_api-1.4.0-py3-none-any.whl

Download URL seventhings_customer_api-1.4.0-py3-none-any.whl
Size 40.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
dd81c010076b1f2f42efa3aa06afd5ef1b196c4ae1d53085f3f624515d868f56
BLAKE2b-256 checksum
How to use checksums
29aaa3cf182115e4948f6f3c974401fb1d568e7408d5c68e982dc21e419578d5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.4.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page