---
sidebar:
  hidden: true
title: roboto.domain.devices.device
---
## Module Contents

### Device

```python
class roboto.domain.devices.device.Device(
    record: roboto.domain.devices.record.DeviceRecord,
    roboto_client: Optional[roboto.http.RobotoClient] = None,
)
```

`from roboto import Device`

[Source](https://github.com/roboto-ai/roboto-python-sdk/blob/main/src/roboto/domain/devices/device.py#L46-L1006)

A device is a non-human entity that can interact with Roboto on behalf of an organization.

Devices represent robots, systems, or other non-human entities that need to authenticate and interact with the Roboto platform. Each device is uniquely identified by a device_id within its organization and can be assigned API tokens for secure authentication.

Common device types include:

- Robots that upload log data directly from their onboard software
- Automated upload stations that collect and transmit data from multiple sources
- Edge computing devices that process and forward data to Roboto

Devices are associated with [`Org`](/reference/python-sdk/roboto/domain/orgs/org#roboto.domain.orgs.org.Org) entities and can create [`Token`](/reference/python-sdk/roboto/domain/tokens/token#roboto.domain.tokens.token.Token) objects for authentication. The underlying data is stored in [`DeviceRecord`](/reference/python-sdk/roboto/domain/devices/record#roboto.domain.devices.record.DeviceRecord) objects for wire transmission.

Device IDs are typically meaningful identifiers like serial numbers, asset tags, or other organization-specific naming schemes that help identify the physical or logical entity in the real world.

> **Note**
>
> Devices cannot be instantiated directly through the constructor. Use the class methods [`create()`](/reference/python-sdk/roboto/domain/devices/device#roboto.domain.devices.device.Device.create), [`from_id()`](/reference/python-sdk/roboto/domain/devices/device#roboto.domain.devices.device.Device.from_id), [`get_or_create()`](/reference/python-sdk/roboto/domain/devices/device#roboto.domain.devices.device.Device.get_or_create), or [`for_org()`](/reference/python-sdk/roboto/domain/devices/device#roboto.domain.devices.device.Device.for_org) to obtain Device instances.

**Parameters**

- **record** (`roboto.domain.devices.record.DeviceRecord`)
- **roboto_client** (`Optional[roboto.http.RobotoClient]`)

**Properties**

- **Device.created** (`datetime.datetime`): The timestamp when this device was registered with Roboto.

- **Device.created_by** (`str`): The user ID of the person who registered this device.

- **Device.custom_fields** (`dict[str, Any]`): Custom-field values defined on Devices in this org.

  Every `Ready` [`CustomField`](/reference/python-sdk/roboto/domain/custom_fields/custom_field#roboto.domain.custom_fields.custom_field.CustomField) defined for `(org_id, Device)` appears as a key. Values that have not been set on this device surface as `None` rather than being absent. Empty when no custom fields are defined for the org.

  A [`Timestamp`](/reference/python-sdk/roboto/domain/custom_fields/record#roboto.domain.custom_fields.record.CustomFieldType.Timestamp) value is returned as an ISO 8601 string.

- **Device.device_id** (`str`): This device's ID. Device ID is a user-provided identifier for a device, which is unique within the device's org.

- **Device.encoded_device_id** (`str`): The device ID, URL-encoded. This is useful for constructing URLs to Roboto APIs which contain the device ID.

- **Device.files** (`roboto.domain.files.FileSystem`): The files associated with this device: its calibrations, part manifests, and the like.

  These are the device's own files, distinct from the dataset files whose `device_id` names this device as the one that recorded them.

  **Raises**

  - [`RobotoNotReadyException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoNotReadyException): The device has no `universal_device_id`, which only a Roboto deployment that predates device files returns.

  **Returns**

  - `roboto.domain.files.FileSystem`

- **Device.metadata** (`dict[str, Any]`): Key-value metadata pairs associated with this device.

- **Device.modified** (`datetime.datetime`): The timestamp when this device record was last modified.

- **Device.modified_by** (`str`): The user ID of the person who last modified this device record.

- **Device.org_id** (`str`): The ID of the org to which this device belongs.

- **Device.record** (`roboto.domain.devices.record.DeviceRecord`): Underlying [`DeviceRecord`](/reference/python-sdk/roboto/domain/devices/record#roboto.domain.devices.record.DeviceRecord) for this device.

  This is the wire representation used in API requests and may evolve over time; prefer the public [`Device`](/reference/python-sdk/roboto/domain/devices/device#roboto.domain.devices.device.Device) API unless you need direct access to the record.

- **Device.tags** (`list[str]`): List of tags associated with this device.

#### Device.clear_custom_field()

```python
def clear_custom_field(name: str) -> Device
```

[Source](https://github.com/roboto-ai/roboto-python-sdk/blob/main/src/roboto/domain/devices/device.py#L923-L925)

Clear a single custom-field value on this device to `None`.

**Parameters**

- **name** (`str`)

**Returns**

- `Device`

#### Device.clear_custom_fields()

```python
def clear_custom_fields(names: collections.abc.Sequence[str]) -> Device
```

[Source](https://github.com/roboto-ai/roboto-python-sdk/blob/main/src/roboto/domain/devices/device.py#L938-L940)

Clear multiple custom-field values on this device to `None`.

**Parameters**

- **names** (`collections.abc.Sequence[str]`)

**Returns**

- `Device`

#### Device.create()

```python
@classmethod
def create(
    device_id: str,
    metadata: Optional[dict[str, Any]] = None,
    tags: Optional[list[str]] = None,
    custom_fields: Optional[dict[str, Any]] = None,
    caller_org_id: Optional[str] = None,
    roboto_client: Optional[roboto.http.RobotoClient] = None,
) -> Device
```

[Source](https://github.com/roboto-ai/roboto-python-sdk/blob/main/src/roboto/domain/devices/device.py#L78-L145)

Register a new device with the Roboto platform.

Creates a new device entity that can authenticate and interact with Roboto on behalf of the specified organization. The device_id must be unique within the organization.

**Parameters**

- **device_id** (`str`): A user-provided identifier for the device, unique within the organization. This is typically a meaningful identifier like a serial number, asset tag, or other organization-specific naming scheme.
- **metadata** (`Optional[dict[str, Any]]`): Optional key-value pairs to associate with the device for discovery and search. For example: {"model": "mk2", "serial_number": "SN001234"}.
- **tags** (`Optional[list[str]]`): Optional list of tags to associate with the device for discovery and organization. For example: ["production", "warehouse-a"].
- **custom_fields** (`Optional[dict[str, Any]]`): Optional initial values for Ready custom fields defined on Devices in the caller's org. Keys must match Ready field names; values must satisfy each field's declared type.
- **caller_org_id** (`Optional[str]`): The organization ID to register the device under. If not specified and the caller belongs to only one organization, that organization will be used. Required if the caller belongs to multiple organizations.
- **roboto_client** (`Optional[roboto.http.RobotoClient]`): Optional RobotoClient instance for API communication. If not provided, the default client configuration will be used.

**Returns**

- `Device`: A Device instance representing the newly registered device.

**Raises**

- [`RobotoConflictException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoConflictException): If a device with the same device_id already exists in the specified organization.
- [`RobotoUnauthorizedException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoUnauthorizedException): If the caller lacks permission to create devices in the specified organization.
- [`RobotoInvalidRequestException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoInvalidRequestException): If the device_id is invalid or the organization ID is malformed.

**Usage**

Register a robot device:

```python
device = Device.create(device_id="robot_001", caller_org_id="og_abc123")
print(f"Registered device: {device.device_id}")
# Registered device: robot_001
```

Register an upload station:

```python
device = Device.create(device_id="upload_station_alpha")
print(f"Device org: {device.org_id}")
# Device org: og_xyz789
```

#### Device.create_session()

```python
def create_session(
    name: str,
    description: Optional[str] = None,
    metadata: Optional[dict[str, Any]] = None,
    tags: Optional[collections.abc.Sequence[str]] = None,
    custom_fields: Optional[dict[str, Any]] = None,
    anchor: Optional[roboto.time.Time] = None,
    files: Optional[collections.abc.Sequence[roboto.experimental.sessions.SessionFile]] = None,
) -> roboto.experimental.sessions.Session
```

[Source](https://github.com/roboto-ai/roboto-python-sdk/blob/main/src/roboto/domain/devices/device.py#L417-L534)

Create one Session on this Device, optionally with its files, topics, and schemas.

The one-session form of [`create_sessions()`](/reference/python-sdk/roboto/domain/devices/device#roboto.domain.devices.device.Device.create_sessions), taking a single declaration's fields as arguments and sharing its semantics: the Session, its file attachments, its topics, and its time ranges are created together or not at all, and every file the declaration names must already be uploaded. `name` identifies the Session within this Device, so resending the same call is safe; the platform reuses the Session already registered under that name instead of creating a second one. Arguments left at their defaults are left out of the request, so a call that reuses an existing Session never overwrites attributes it does not name. To create a Session with no name, or one spanning several Devices, use [`create()`](/reference/python-sdk/roboto/experimental/sessions/session#roboto.experimental.sessions.session.Session.create).

A declaration the platform refuses raises here. Only [`create_sessions()`](/reference/python-sdk/roboto/domain/devices/device#roboto.domain.devices.device.Device.create_sessions) reports a refusal instead of raising it, because only a batch has positions to trace refusals back to.

Declared times are stored exactly as given, in each file's own timestamps, and read as nanoseconds since the Unix epoch; the platform never invents a wall-clock time. To place the Session at the wall-clock time it happened, supply `anchor`, or call [`set_unix_offset()`](/reference/python-sdk/roboto/experimental/sessions/session#roboto.experimental.sessions.session.Session.set_unix_offset) later.

**Parameters**

- **name** (`str`): Name of the Session, unique within this Device (max 120 characters).
- **description** (`Optional[str]`): Optional description of the Session.
- **metadata** (`Optional[dict[str, Any]]`): Optional initial metadata. Sessions are not filterable or sortable by `metadata` keys; for queryable structured attributes, define a custom field on the `Session` entity type.
- **tags** (`Optional[collections.abc.Sequence[str]]`): Optional initial tags. Sessions can be filtered by tag membership but are not sortable by tag.
- **custom_fields** (`Optional[dict[str, Any]]`): Optional initial values for Ready custom fields defined on Sessions in this Device's org. Keys must match Ready field names; values must satisfy each field's declared type.
- **anchor** (`Optional[roboto.time.Time]`): Optional wall-clock anchor, the real-world instant at which the declared data's time 0 occurred. An `int` is nanoseconds since the Unix epoch; any other [`Time`](/reference/python-sdk/roboto/time#roboto.time.Time) is read as [`to_epoch_nanoseconds()`](/reference/python-sdk/roboto/time#roboto.time.to_epoch_nanoseconds) reads it (a `datetime` or ISO 8601 string is that instant; a `float`, `Decimal`, or numeric string is seconds since the epoch). It applies to every file entry that does not carry its own [`anchor_ns`](/reference/python-sdk/roboto/experimental/sessions/operations#roboto.experimental.sessions.operations.FileDeclaration.anchor_ns).
- **files** (`Optional[collections.abc.Sequence[roboto.experimental.sessions.SessionFile]]`): Files composing this Session, with the topics whose data each one carries. Every file must already be uploaded, and may appear at most once. Files can also be included after creation with [`add_file()`](/reference/python-sdk/roboto/experimental/sessions/session#roboto.experimental.sessions.session.Session.add_file) or [`add_files()`](/reference/python-sdk/roboto/experimental/sessions/session#roboto.experimental.sessions.session.Session.add_files).

**Returns**

- `roboto.experimental.sessions.Session`: The created Session.

**Raises**

- `TypeError`: If `anchor` is not one of the [`Time`](/reference/python-sdk/roboto/time#roboto.time.Time) types.
- `ValueError`: If `anchor` is a boolean, a negative number (an `int`, `float`, `Decimal`, or numeric string), or a string that is neither a number of seconds nor an ISO 8601 timestamp. Raised before anything is sent to the platform.
- `OverflowError`: If `anchor` is an infinite `float`, `Decimal`, or string, such as `"inf"`. Raised before anything is sent to the platform.
- `pydantic.ValidationError`: If `name` is empty or longer than 120 characters, `anchor` does not fall after the Unix epoch or is too large for a signed 64-bit integer of nanoseconds, a file appears in more than one entry, `files` declares more than [`MAX_FILES_AND_TOPICS_PER_REQUEST`](/reference/python-sdk/roboto/experimental/ingest/operations#roboto.experimental.ingest.operations.MAX_FILES_AND_TOPICS_PER_REQUEST) files and topics combined, or representations name one file in two storage formats. Raised while the request is being built, before anything is sent to the platform.
- [`RobotoInvalidRequestException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoInvalidRequestException): If the platform refuses the declaration, either because it contradicts data the platform already holds or because it carries a value the platform rejects, such as a `custom_fields` value that does not satisfy its field's declared type. No Session, file attachment, topic, or time range is created; the topic identifiers and schema definitions the declaration resolved stay stored, and a resend reuses them.
- [`RobotoConflictException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoConflictException): If something the declaration was prepared against changed while it was being written. Nothing is created; resending is the fix.
- [`RobotoNotFoundException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoNotFoundException): If this Device is no longer registered, the `file_id` of a file entry, or of a representation one of its topics lists, does not name a file in this Device's organization whose status is [`Available`](/reference/python-sdk/roboto/domain/files/record#roboto.domain.files.record.FileStatus.Available), or the declaration names something else that does not exist, such as a `custom_fields` key naming a custom field the organization does not define on Sessions. Nothing is created.
- [`RobotoUnauthorizedException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoUnauthorizedException): If the caller lacks permission to create Sessions on this Device, or to edit a file the declaration declares topics on, a file it anchors, or a file a listed representation names, or lacks topic edit access in this Device's organization while the declaration states `is_default_for_reads` on a timeline source.
- [`RobotoUnrecognizedErrorException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoUnrecognizedErrorException): If the platform refuses the declaration under an error code this SDK release does not define. Carries the code and message the platform sent.

**Usage**

Create a Session and add a file to it:

```python
device = Device.from_id("robot_001", org_id="og_abc123")
session = device.create_session(name="2024-05-01_morning_run")
session.add_file("fl_0123456789abcdef")
```

Create a Session placed at the wall-clock time it was recorded:

```python
import datetime
session = device.create_session(
    name="2024-05-01_morning_run",
    anchor=datetime.datetime(2024, 5, 1, 9, 30, tzinfo=datetime.timezone.utc),
)
```

#### Device.create_sessions()

```python
def create_sessions(
    sessions: collections.abc.Sequence[roboto.experimental.sessions.SessionDeclaration],
) -> roboto.http.BatchResponse[roboto.experimental.sessions.Session]
```

[Source](https://github.com/roboto-ai/roboto-python-sdk/blob/main/src/roboto/domain/devices/device.py#L537-L695)

Create many Sessions on this Device, each with its files, topics, and schemas, in one call.

Each call accepts up to [`MAX_SESSIONS_PER_REQUEST`](/reference/python-sdk/roboto/experimental/sessions/operations#roboto.experimental.sessions.operations.MAX_SESSIONS_PER_REQUEST) declarations, one per Session (e.g. the episodes of a LeRobot dataset), and up to [`MAX_FILES_AND_TOPICS_PER_REQUEST`](/reference/python-sdk/roboto/experimental/ingest/operations#roboto.experimental.ingest.operations.MAX_FILES_AND_TOPICS_PER_REQUEST) files and topics combined, counted across every declaration; split anything larger across several calls. Every file a declaration names must already be uploaded; [`upload_files()`](/reference/python-sdk/roboto/domain/datasets/dataset#roboto.domain.datasets.dataset.Dataset.upload_files) returns the file IDs it creates, and files from any number of datasets may appear in one batch.

The platform decides which declarations to refuse before writing anything, then writes the rest together. A declaration's Session, file attachments, topics, and time ranges are created together or not at all, and a declaration the platform refuses leaves the others written as if it were absent. None of the declarations is written when a failure the platform did not anticipate, such as a timeout, interrupts the call, or when a Session a declaration reuses is deleted before the call completes, which raises [`RobotoNotFoundException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoNotFoundException). Each declaration is written as it would be had the ones before it been sent as calls of their own: a later declaration anchoring data an earlier one holds moves the earlier Session's time range with it. A refused declaration, or a call that fails, still leaves behind the topic identifiers and schema definitions it resolved, which a resend reuses.

Check [`failed`](/reference/python-sdk/roboto/http/response#roboto.http.response.BatchResponse.failed) before treating the batch as done. Each entry there is the [`RobotoDomainException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoDomainException) the platform refused a declaration with, so `isinstance` tells the reasons apart; a refusal under an error code this SDK release does not define arrives as [`RobotoUnrecognizedErrorException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoUnrecognizedErrorException).

Resending the same call is safe. This Device plus each declaration's `name` identifies the Session the declaration creates or reuses, so a resend fills in only what is missing rather than duplicating what an earlier attempt created.

Declared times are stored exactly as given, in each file's own timestamps, and read as nanoseconds since the Unix epoch; the platform never invents a wall-clock time. A recording whose timestamps start at 0 therefore sits at the epoch until it is anchored. To place a Session at the wall-clock time it happened, supply [`anchor_ns`](/reference/python-sdk/roboto/experimental/sessions/operations#roboto.experimental.sessions.operations.SessionDeclaration.anchor_ns), or call [`set_unix_offset()`](/reference/python-sdk/roboto/experimental/sessions/session#roboto.experimental.sessions.session.Session.set_unix_offset) later.

**Parameters**

- **sessions** (`collections.abc.Sequence[roboto.experimental.sessions.SessionDeclaration]`): One declaration per Session to create. An empty sequence returns an empty response without contacting the platform.

**Returns**

- `roboto.http.BatchResponse[roboto.experimental.sessions.Session]`: A [`BatchResponse`](/reference/python-sdk/roboto/http/response#roboto.http.response.BatchResponse) with one element per declaration, in request order, holding either the Session the declaration created or why the platform refused it. A declaration naming a Session this Device already holds yields that Session rather than a second one.

**Raises**

- `pydantic.ValidationError`: If more than [`MAX_SESSIONS_PER_REQUEST`](/reference/python-sdk/roboto/experimental/sessions/operations#roboto.experimental.sessions.operations.MAX_SESSIONS_PER_REQUEST) declarations are given, the batch declares more than [`MAX_FILES_AND_TOPICS_PER_REQUEST`](/reference/python-sdk/roboto/experimental/ingest/operations#roboto.experimental.ingest.operations.MAX_FILES_AND_TOPICS_PER_REQUEST) files and topics combined, the same session name is declared more than once, or representations name one file in two storage formats. All are enforced when the request body is constructed, before anything is sent to the platform.
- [`RobotoInvalidRequestException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoInvalidRequestException): If the batch is malformed. Nothing is created.
- [`RobotoNotFoundException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoNotFoundException): If this Device is no longer registered, the `file_id` of a file entry, or of a representation one of its topics lists, does not name a file in this Device's organization whose status is [`Available`](/reference/python-sdk/roboto/domain/files/record#roboto.domain.files.record.FileStatus.Available), or a Session a declaration reuses is deleted before the call completes. Nothing is created.
- [`RobotoUnauthorizedException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoUnauthorizedException): If the caller lacks permission to create Sessions on this Device, or to edit a file a declaration declares topics on, a file it anchors, or a file a listed representation names, or lacks topic edit access in this Device's organization while a declaration states `is_default_for_reads` on a timeline source.

**Usage**

Register two chunks of one recording as a single Session, each chunk's topic data read from the chunk itself:

```python
import pathlib
from roboto.domain.datasets import Dataset
from roboto.domain.devices import Device
from roboto.domain.topics import CanonicalDataType, RepresentationStorageFormat
from roboto.experimental.ingest import (
    Field,
    McapLogTimeSource,
    RepresentationDeclaration,
    Schema,
    TopicDeclaration,
)
from roboto.experimental.sessions import SessionDeclaration, SessionFile
imu_schema = Schema(
    name="sensor_msgs/msg/Imu",
    fields=[
        Field(
            name="angular_velocity_x",
            data_type="float64",
            canonical_data_type=CanonicalDataType.Number,
        ),
    ],
)
dataset = Dataset.from_id("ds_0123456789ab")
device = Device.from_id("robot_001")
chunks = [pathlib.Path("recording/chunk_0000.mcap"), pathlib.Path("recording/chunk_0001.mcap")]
file_ids = dataset.upload_files(chunks)
batch = device.create_sessions(
    [
        SessionDeclaration(
            name="morning_drive",
            files=[
                SessionFile(
                    file_id=file_ids[chunks[0]],
                    topics=[
                        TopicDeclaration(
                            topic_name="/imu",
                            topic_schema=imu_schema,
                            timeline_sources=[
                                McapLogTimeSource(
                                    min_file_timestamp_ns=1_785_974_400_000_000_000,
                                    max_file_timestamp_ns=1_785_974_404_000_000_000,
                                ),
                            ],
                            representations=[
                                RepresentationDeclaration(
                                    file_id=file_ids[chunks[0]],
                                    storage_format=RepresentationStorageFormat.MCAP,
                                ),
                            ],
                        ),
                    ],
                ),
                SessionFile(
                    file_id=file_ids[chunks[1]],
                    topics=[
                        TopicDeclaration(
                            topic_name="/imu",
                            topic_schema=imu_schema,
                            timeline_sources=[
                                McapLogTimeSource(
                                    min_file_timestamp_ns=1_785_974_404_000_000_000,
                                    max_file_timestamp_ns=1_785_974_408_000_000_000,
                                ),
                            ],
                            representations=[
                                RepresentationDeclaration(
                                    file_id=file_ids[chunks[1]],
                                    storage_format=RepresentationStorageFormat.MCAP,
                                ),
                            ],
                        ),
                    ],
                ),
            ],
        ),
    ],
)
batch.failed
# []
```

#### Device.create_token()

```python
def create_token(
    expiry_days: int = 366,
    name: Optional[str] = None,
    description: Optional[str] = None,
    api_scopes: Optional[collections.abc.Collection[roboto.auth.scope.ApiScope]] = None,
) -> tuple[roboto.domain.tokens.Token, str]
```

[Source](https://github.com/roboto-ai/roboto-python-sdk/blob/main/src/roboto/domain/devices/device.py#L697-L770)

Create an authentication token for this device.

Generates a new API token that can be used to authenticate requests made on behalf of this device. The token secret is returned only once and cannot be retrieved again, so it must be stored securely by the caller.

**Parameters**

- **expiry_days** (`int`): Number of days until the token expires. Defaults to 366 days (1 year). Must be a positive integer.
- **name** (`Optional[str]`): Human-readable name for the token. If not provided, defaults to "{org_id}\_{device_id}" format.
- **description** (`Optional[str]`): Optional description explaining the token's purpose or usage context.
- **api_scopes** (`Optional[collections.abc.Collection[roboto.auth.scope.ApiScope]]`): Optional set of API scopes to limit the token's permissions. If not provided, the token will have full access to all APIs.

**Returns**

- `tuple[roboto.domain.tokens.Token, str]`: A tuple containing:

  - Token: The Token object representing the created token
  - str: The secret token value (only available at creation time)

**Raises**

- [`RobotoDomainException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoDomainException): If token creation fails or the secret is not returned by the server (this should never happen under normal circumstances).
- [`RobotoUnauthorizedException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoUnauthorizedException): If the caller lacks permission to create tokens for this device.

**Usage**

Create a token with default settings:

```python
device = Device.from_id("robot_001", org_id="og_abc123")
token, secret = device.create_token()
print(f"Token created: {token.token_id}")
print(f"Secret (save this!): {secret}")
# Token created: to_abc123def456
# Secret (save this!): robo_pat_abc123def456...
```

Create a token with custom expiry and description:

```python
token, secret = device.create_token(
    expiry_days=30, name="Monthly Upload Token", description="Token for automated monthly data uploads"
)
print(f"Token expires in 30 days: {token.token_id}")
# Token expires in 30 days: to_def789ghi012
```

#### Device.delete()

```python
def delete(keep_files: bool = False) -> None
```

[Source](https://github.com/roboto-ai/roboto-python-sdk/blob/main/src/roboto/domain/devices/device.py#L772-L811)

Delete this device from the Roboto platform.

Permanently removes this device and all associated tokens. This action cannot be undone. Any tokens created for this device will be immediately invalidated.

The device's own files ([`files`](/reference/python-sdk/roboto/domain/devices/device#roboto.domain.devices.device.Device.files)) are deleted with it, every version of each, shortly after this call returns. With `keep_files=True` they move to the org root instead, under `devices/<universal_device_id>/`, keeping their file IDs and every version, so they stay reachable through `org.files`. Links among the device's files are deleted in both cases, never moved; their targets are left alone.

**Parameters**

- **keep_files** (`bool`): Move the device's files to the org root instead of deleting them. Requires permission to upload files to the org.

**Raises**

- [`RobotoUnauthorizedException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoUnauthorizedException): If the caller lacks permission to delete this device.
- [`RobotoNotFoundException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoNotFoundException): If the device has already been deleted or does not exist.

**Returns**

- `None`

**Usage**

Delete a device after confirming its identity:

```python
device = Device.from_id("old_robot_001")
print(f"Deleting device: {device.device_id}")
device.delete()
print("Device deleted successfully")
# Deleting device: old_robot_001
# Device deleted successfully
```

Delete a device but keep its calibrations and manifests in the org root:

```python
device = Device.from_id("old_robot_001")
device.delete(keep_files=True)
```

#### Device.for_org()

```python
@classmethod
def for_org(
    org_id: str,
    roboto_client: Optional[roboto.http.RobotoClient] = None,
) -> collections.abc.Generator[Device, None, None]
```

[Source](https://github.com/roboto-ai/roboto-python-sdk/blob/main/src/roboto/domain/devices/device.py#L148-L203)

List all devices registered for a given organization.

Retrieves all devices that belong to the specified organization. For organizations with large numbers of devices, this method uses pagination and yields results as they become available from the API.

**Parameters**

- **org_id** (`str`): The organization ID to list devices for.
- **roboto_client** (`Optional[roboto.http.RobotoClient]`): Optional RobotoClient instance for API communication. If not provided, the default client configuration will be used.

**Returns**

- `collections.abc.Generator[Device, None, None]`: A generator of Device objects. For organizations with many devices, this may involve multiple service calls, and the generator will yield results as they become available.

**Raises**

- [`RobotoUnauthorizedException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoUnauthorizedException): If the caller lacks permission to list devices in the specified organization.
- [`RobotoNotFoundException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoNotFoundException): If the specified organization does not exist.

**Usage**

List all devices in an organization:

```python
for device in Device.for_org("og_abc123"):
    print(f"Device: {device.device_id} (created: {device.created})")
# Device: robot_001 (created: 2024-01-15 10:30:00)
# Device: upload_station_beta (created: 2024-01-17 09:15:00)
```

Count devices in an organization:

```python
device_count = sum(1 for _ in Device.for_org("og_abc123"))
print(f"Total devices: {device_count}")
# Total devices: 2
```

#### Device.from_id()

```python
@classmethod
def from_id(
    device_id: str,
    roboto_client: Optional[roboto.http.RobotoClient] = None,
    org_id: Optional[str] = None,
) -> Device
```

[Source](https://github.com/roboto-ai/roboto-python-sdk/blob/main/src/roboto/domain/devices/device.py#L206-L255)

Retrieve a device by its device ID.

Looks up and returns a Device instance for the specified device_id. The device_id must be unique within the organization scope.

**Parameters**

- **device_id** (`str`): The device ID to look up. This is the user-provided identifier that was specified when the device was created.
- **roboto_client** (`Optional[roboto.http.RobotoClient]`): Optional RobotoClient instance for API communication. If not provided, the default client configuration will be used.
- **org_id** (`Optional[str]`): The organization ID that owns the device. If not specified and the caller belongs to only one organization, that organization will be used. Required if the caller belongs to multiple organizations.

**Returns**

- `Device`: A Device object representing the specified device.

**Raises**

- [`RobotoNotFoundException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoNotFoundException): If the specified device is not registered with Roboto or does not exist in the specified organization.
- [`RobotoUnauthorizedException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoUnauthorizedException): If the caller lacks permission to access the device or the specified organization.
- [`RobotoInvalidRequestException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoInvalidRequestException): If the device_id or org_id parameters are malformed.

**Usage**

Retrieve a device by ID with explicit organization:

```python
device = Device.from_id("robot_001", org_id="og_abc123")
print(f"Device: {device.device_id} in org {device.org_id}")
# Device: robot_001 in org og_abc123
```

Retrieve a device:

```python
device = Device.from_id("upload_station_alpha")
print(f"Found device created by: {device.created_by}")
# Found device created by: user@example.com
```

#### Device.get_or_create()

```python
@classmethod
def get_or_create(
    device_id: str,
    metadata: Optional[dict[str, Any]] = None,
    tags: Optional[list[str]] = None,
    custom_fields: Optional[dict[str, Any]] = None,
    caller_org_id: Optional[str] = None,
    roboto_client: Optional[roboto.http.RobotoClient] = None,
) -> Device
```

[Source](https://github.com/roboto-ai/roboto-python-sdk/blob/main/src/roboto/domain/devices/device.py#L259-L309)

Register a device, or return the existing one if `device_id` is already taken.

`metadata`, `tags`, and `custom_fields` are applied only by the call that registers the device; a device that is already registered comes back unchanged.

**Parameters**

- **device_id** (`str`): A user-provided identifier for the device, unique within the organization.
- **metadata** (`Optional[dict[str, Any]]`): Optional key-value pairs to associate with the device on first registration.
- **tags** (`Optional[list[str]]`): Optional tags to associate with the device on first registration.
- **custom_fields** (`Optional[dict[str, Any]]`): Optional initial values for Ready custom fields, applied on first registration.
- **caller_org_id** (`Optional[str]`): The organization the device belongs to. Required if the caller belongs to multiple organizations.
- **roboto_client** (`Optional[roboto.http.RobotoClient]`): Optional RobotoClient instance for API communication.

**Returns**

- `Device`: The newly registered or pre-existing Device.

**Raises**

- [`RobotoUnauthorizedException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoUnauthorizedException): If the caller lacks permission to create devices in, or read devices from, the specified organization.
- [`RobotoInvalidRequestException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoInvalidRequestException): If the device_id is invalid or the organization ID is malformed.
- [`RobotoNotFoundException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoNotFoundException): If the device is deleted between the registration attempt and the lookup that follows it. Those are two calls rather than one atomic operation, so the race is possible, though unlikely.

**Usage**

```python
device = Device.get_or_create(device_id="aloha_001")
device.device_id
# 'aloha_001'
```

#### Device.list_sessions()

```python
def list_sessions() -> collections.abc.Generator[roboto.experimental.sessions.Session, None, None]
```

[Source](https://github.com/roboto-ai/roboto-python-sdk/blob/main/src/roboto/domain/devices/device.py#L814-L843)

Iterate all Sessions attached to this Device.

Yields results as they are returned from the server, paginating transparently.

**Usage**

Print the name of every Session for a Device:

```python
device = Device.from_id("robot_001", org_id="og_abc123")
for session in device.list_sessions():
    print(session.name)
```

**Returns**

- `collections.abc.Generator[roboto.experimental.sessions.Session, None, None]`

#### Device.put_metadata()

```python
def put_metadata(metadata: dict[str, Any]) -> Device
```

[Source](https://github.com/roboto-ai/roboto-python-sdk/blob/main/src/roboto/domain/devices/device.py#L845-L861)

Add or update metadata fields for this device.

**Parameters**

- **metadata** (`dict[str, Any]`): Key-value pairs to add or update in the device's metadata. Existing keys will be overwritten, new keys will be added.

**Returns**

- `Device`: Updated Device instance with the new metadata.

**Usage**

```python
device = Device.from_id("robot_001")
updated_device = device.put_metadata({"firmware_version": "2.1.0", "location": "warehouse-b"})
print(updated_device.metadata["firmware_version"])
# 2.1.0
```

#### Device.put_tags()

```python
def put_tags(tags: list[str]) -> Device
```

[Source](https://github.com/roboto-ai/roboto-python-sdk/blob/main/src/roboto/domain/devices/device.py#L863-L878)

Add tags to this device.

**Parameters**

- **tags** (`list[str]`): List of tags to add to the device. Duplicate tags will be ignored.

**Returns**

- `Device`: Updated Device instance with the new tags added.

**Usage**

```python
device = Device.from_id("robot_001")
updated_device = device.put_tags(["production", "warehouse-c"])
print("production" in updated_device.tags)
# True
```

#### Device.remove_metadata()

```python
def remove_metadata(keys: list[str]) -> Device
```

[Source](https://github.com/roboto-ai/roboto-python-sdk/blob/main/src/roboto/domain/devices/device.py#L880-L893)

Remove metadata fields from this device.

**Parameters**

- **keys** (`list[str]`): List of metadata keys to remove from the device.

**Returns**

- `Device`: Updated Device instance with the specified metadata keys removed.

**Usage**

```python
device = Device.from_id("robot_001")
updated_device = device.remove_metadata(["old_field", "deprecated_key"])
```

#### Device.remove_tags()

```python
def remove_tags(tags: list[str]) -> Device
```

[Source](https://github.com/roboto-ai/roboto-python-sdk/blob/main/src/roboto/domain/devices/device.py#L895-L908)

Remove tags from this device.

**Parameters**

- **tags** (`list[str]`): List of tags to remove from the device.

**Returns**

- `Device`: Updated Device instance with the specified tags removed.

**Usage**

```python
device = Device.from_id("robot_001")
updated_device = device.remove_tags(["old_tag", "deprecated"])
```

#### Device.set_custom_field()

```python
def set_custom_field(name: str, value: Any) -> Device
```

[Source](https://github.com/roboto-ai/roboto-python-sdk/blob/main/src/roboto/domain/devices/device.py#L911-L920)

Set a single custom-field value on this device.

`name` must be the name of a [`Ready`](/reference/python-sdk/roboto/domain/custom_fields/record#roboto.domain.custom_fields.record.CustomFieldStatus.Ready) custom field for this device's org and the [`Device`](/reference/python-sdk/roboto/domain/custom_fields/record#roboto.domain.custom_fields.record.TargetEntityType.Device) entity type; `value` must satisfy the field's declared type.

**Parameters**

- **name** (`str`)
- **value** (`Any`)

**Returns**

- `Device`

#### Device.set_custom_fields()

```python
def set_custom_fields(fields: dict[str, Any]) -> Device
```

[Source](https://github.com/roboto-ai/roboto-python-sdk/blob/main/src/roboto/domain/devices/device.py#L928-L935)

Set or overwrite multiple custom-field values on this device.

Each key must name a Ready custom field for this device's org and the [`Device`](/reference/python-sdk/roboto/domain/custom_fields/record#roboto.domain.custom_fields.record.TargetEntityType.Device) entity type; each value must satisfy the field's declared type.

**Parameters**

- **fields** (`dict[str, Any]`)

**Returns**

- `Device`

#### Device.tokens()

```python
def tokens() -> collections.abc.Sequence[roboto.domain.tokens.Token]
```

[Source](https://github.com/roboto-ai/roboto-python-sdk/blob/main/src/roboto/domain/devices/device.py#L942-L979)

Retrieve all authentication tokens associated with this device.

Returns a list of all tokens that have been created for this device, including both active and expired tokens. The token secrets are not included in the response as they are only available at creation time.

**Returns**

- `collections.abc.Sequence[roboto.domain.tokens.Token]`: A sequence of Token objects representing all tokens created for this device. The sequence may be empty if no tokens have been created.

**Raises**

- [`RobotoUnauthorizedException`](/reference/python-sdk/roboto/exceptions/domain#roboto.exceptions.domain.RobotoUnauthorizedException): If the caller lacks permission to list tokens for this device.

**Usage**

List all tokens for a device:

```python
device = Device.from_id("robot_001")
tokens = device.tokens()
for token in tokens:
    print(f"Token: {token.token_id}")
# Token: to_abc123def456
# Token: to_ghi789jkl012
```

Check if device has any tokens:

```python
device = Device.from_id("new_robot")
if device.tokens():
    print("Device has tokens")
else:
    print("No tokens found for device")
# No tokens found for device
```

#### Device.update()

```python
def update(request: roboto.domain.devices.operations.UpdateDeviceRequest) -> Device
```

[Source](https://github.com/roboto-ai/roboto-python-sdk/blob/main/src/roboto/domain/devices/device.py#L981-L1006)

Update device properties using a structured request.

**Parameters**

- **request** (`roboto.domain.devices.operations.UpdateDeviceRequest`): UpdateDeviceRequest containing the changes to apply.

**Returns**

- `Device`: Updated Device instance with the changes applied.

**Usage**

```python
from roboto.updates import MetadataChangeset
device = Device.from_id("robot_001")
updated_device = device.update(
    UpdateDeviceRequest(
        metadata_changeset=MetadataChangeset(
            put_fields={"version": "2.0"}, put_tags=["updated"], remove_tags=["old"]
        )
    )
)
```
