diff --git a/claude.md b/claude.md
new file mode 100644
index 000000000..4d2e9b92c
--- /dev/null
+++ b/claude.md
@@ -0,0 +1,781 @@
+# Seam API Documentation - Repository Guide
+
+This document provides a comprehensive overview of the Seam API documentation repository structure, organization, and writing conventions. Use this as a reference when working with the documentation.
+
+**Repository:** Seam API Documentation (hosted on GitBook at https://docs.seam.co/latest/)
+**Total Files:** 627 markdown files
+**Last Updated:** 2026-01-28
+
+---
+
+## Table of Contents
+
+1. [Documentation Organization](#documentation-organization)
+2. [File Structure](#file-structure)
+3. [Writing Style & Tone](#writing-style--tone)
+4. [Content Types](#content-types)
+5. [GitBook Configuration](#gitbook-configuration)
+6. [Common Patterns & Conventions](#common-patterns--conventions)
+7. [Key Guidelines](#key-guidelines)
+
+---
+
+## Documentation Organization
+
+The documentation is organized into **7 main sections**, each serving a distinct purpose:
+
+### 1. Core Concepts (Foundation)
+
+**Purpose:** Foundational knowledge for understanding Seam's architecture and key concepts
+
+**Topics:**
+
+- Overview
+- Seam Console
+- Workspaces
+- Authentication
+- Connect Webviews
+- Devices
+- Providers
+- Connected Accounts
+- Mapping Resources
+- Action Attempts
+
+**Location:** `/docs/core-concepts/`
+
+### 2. Capability Guides (Feature-focused)
+
+**Purpose:** Feature-specific implementation guides organized by what devices can DO
+
+**Topics:**
+
+- Smart Locks
+- Access Control Systems
+- Mobile Access
+- Access Grants & Instant Keys
+- Thermostats
+- Noise Sensors
+- Seam Bridge
+- Customer Portals
+- Reservation Automations
+
+**Location:** `/docs/capability-guides/`
+
+### 3. API Reference (Technical Specifications)
+
+**Purpose:** Complete API endpoint documentation (appears to be auto-generated from OpenAPI specs)
+
+**Topics:**
+
+- Installation
+- Authentication
+- Pagination
+- Detailed endpoint documentation for:
+ - Access Codes
+ - ACS (Access Control Systems)
+ - Devices
+ - Events
+ - Webhooks
+ - Connected Accounts
+ - And 20+ other resources
+
+**Location:** `/docs/api/`
+
+### 4. UI Components
+
+**Purpose:** Frontend component libraries and SDKs
+
+**Topics:**
+
+- Seam Components (React, Angular, Vue)
+- Seam Mobile Components (iOS)
+
+**Location:** `/docs/seam-components/`, `/docs/ui-components/`
+
+### 5. Device & System Integration Guides
+
+**Purpose:** Manufacturer and system-specific setup instructions
+
+**Coverage:** 40+ device manufacturers including:
+
+- Smart locks (August, Yale, Schlage, Kwikset, etc.)
+- Access control systems (ASSA ABLOY, Salto, Brivo, etc.)
+- Thermostats (Ecobee, Honeywell)
+- Other IoT devices
+
+**Location:** `/docs/device-and-system-integration-guides/`, `/docs/device-guides/`
+
+### 6. Developer Tools
+
+**Purpose:** Development workflow and tooling documentation
+
+**Topics:**
+
+- Webhooks
+- Seam CLI
+- Sandbox Devices
+- Rate Limits
+- Mobile SDKs
+
+**Location:** `/docs/developer-tools/`
+
+### 7. Industry Guides
+
+**Purpose:** Vertical-specific use cases and implementation guides
+
+**Current coverage:** Hospitality Industry
+
+**Location:** `/docs/industry-guides/`
+
+---
+
+## File Structure
+
+### Root Level Files
+
+```
+/docs/
+├── README.md # Landing page / home
+├── SUMMARY.md # GitBook table of contents
+├── quickstart.md # 5-minute getting started guide
+└── go-live.md # Production readiness checklist
+```
+
+### Directory Organization
+
+```
+/docs/
+├── core-concepts/ (9 subdirectories)
+├── capability-guides/ (11 subdirectories)
+├── api/ (29 subdirectories - auto-generated)
+├── device-and-system-integration-guides/ (26 subdirectories)
+├── device-guides/ (41 subdirectories)
+├── products/ (4 subdirectories)
+├── seam-components/ (3 subdirectories)
+├── ui-components/ (2 subdirectories)
+├── developer-tools/ (3 subdirectories)
+├── industry-guides/ (1 subdirectory)
+└── .gitbook/
+ ├── assets/ (600+ images)
+ └── includes/ (reusable snippets)
+```
+
+### Asset Organization
+
+**Location:** `/docs/.gitbook/assets/`
+
+**Naming conventions:**
+
+- Descriptive names with hyphens: `august_connect-flow-screens_light.png`
+- Dark/light mode variants: `_dark.png` and `_light.png` suffixes
+- Manufacturer logos, screenshots, diagrams, cover images
+
+**Asset types:**
+
+- Device screenshots
+- UI flow diagrams
+- Timeline visualizations
+- Manufacturer logos
+- Architecture diagrams
+
+---
+
+## Writing Style & Tone
+
+### Tone Characteristics
+
+- **Professional but approachable** - Technical without being overly formal
+- **Direct and action-oriented** - Uses imperatives ("Create", "Install", "Confirm")
+- **Helpful and encouraging** - Includes success hints and congratulatory messages
+- **Developer-first** - Assumes technical audience, provides code-first examples
+
+### Voice & Perspective
+
+- **Second person:** Consistently uses "you" and "your"
+- **Active voice:** Preferred throughout
+- **Present tense:** For describing functionality and actions
+
+### Formatting Characteristics
+
+**Paragraph length:** 2-3 sentences for readability
+
+**List usage:** Extensive bullet lists for scanning
+
+**Code emphasis:** Heavy use of inline code formatting with backticks
+
+**Examples:**
+
+```markdown
+✓ "You can create an access code by making a request to the `access_codes.create` endpoint."
+✗ "An access code can be created through the utilization of the access codes creation endpoint."
+```
+
+---
+
+## Content Types
+
+### 1. Conceptual Documentation
+
+**Purpose:** Explain what something is and why it matters
+
+**Characteristics:**
+
+- Short (typically 50-200 lines)
+- High-level overviews
+- Architecture explanations
+- Links to related guides
+
+**Example:** [core-concepts/overview.md](docs/core-concepts/overview.md)
+
+### 2. Getting Started Guides
+
+**Purpose:** Onboard new users quickly
+
+**Structure:**
+
+1. Prerequisites
+2. Install SDK
+3. Connect device
+4. Perform first action
+5. Build simple application
+
+**Length:** 300-1,500 lines
+
+**Example:** [quickstart.md](docs/quickstart.md)
+
+### 3. How-To Guides (Capability Guides)
+
+**Purpose:** Task-oriented instructions for specific features
+
+**Structure:**
+
+1. Overview
+2. Before You Begin (prerequisites)
+3. Step-by-step instructions
+4. Code examples (multi-language)
+5. Verification steps
+6. Next steps
+
+**Length:** 500-2,000 lines
+
+**Example:** [capability-guides/creating-access-codes/README.md](docs/capability-guides/creating-access-codes/README.md)
+
+### 4. API Reference
+
+**Purpose:** Complete technical specifications for API endpoints
+
+**Structure:**
+
+1. Endpoint description
+2. Request parameters (with types and constraints)
+3. Response schema
+4. Code examples (6+ languages)
+5. Error responses
+
+**Length:** 1,000-2,000 lines
+
+**Characteristics:**
+
+- Appears to be auto-generated
+- Consistent formatting across all endpoints
+- Comprehensive parameter documentation
+
+**Example:** [api/access_codes/create.md](docs/api/access_codes/create.md)
+
+### 5. Integration Guides
+
+**Purpose:** Manufacturer-specific setup and configuration
+
+**Structure:**
+
+1. Device/system overview
+2. Requirements
+3. Setup workflow (often 5-10 steps)
+4. Configuration details
+5. Troubleshooting
+6. Next steps
+
+**Length:** 200-1,500 lines
+
+**Example:** [device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/](docs/device-and-system-integration-guides/assa-abloy-visionline-access-control-system-in-development/)
+
+### 6. Tutorials
+
+**Purpose:** End-to-end implementation guides for specific use cases
+
+**Characteristics:**
+
+- Industry-specific workflows
+- Complete working examples
+- Production considerations
+
+**Example:** [industry-guides/hospitality-industry-guide/](docs/industry-guides/hospitality-industry-guide/)
+
+### 7. Reference Materials
+
+**Purpose:** Quick lookup tables and specifications
+
+**Types:**
+
+- Device requirements tables
+- Sandbox credentials
+- Error code references
+- Capability matrices
+
+---
+
+## GitBook Configuration
+
+### Key Settings (from `.gitbook.yaml`)
+
+**Root directory:** `./docs/`
+
+**Structure files:**
+
+- `README.md` - Landing page
+- `SUMMARY.md` - Table of contents
+
+### Redirect Management
+
+The configuration includes **198+ redirects** maintaining backward compatibility:
+
+**Common redirect patterns:**
+
+```yaml
+# API client paths → Unified API paths
+api-clients/access-codes/create: api/access_codes/create
+
+# Device guides → Integration guides
+device-guides/august-locks: device-and-system-integration-guides/august-locks
+
+# URL normalization
+old/path/with-dashes: new-path/with-underscores
+```
+
+**Purpose:** Ensures old documentation links continue working as structure evolves
+
+---
+
+## Common Patterns & Conventions
+
+### 1. Frontmatter
+
+**Standard format:**
+
+```yaml
+---
+description: >-
+ Brief one-line summary of page content for SEO and navigation
+---
+```
+
+**Usage:** Every page should have descriptive frontmatter
+
+### 2. Page Structure Template
+
+```markdown
+---
+description: Brief page summary
+---
+
+# Page Title
+
+Brief introduction paragraph explaining what this page covers.
+
+## Section 1: Overview or Prerequisites
+
+Content...
+
+## Section 2: Main Content or Step-by-Step Instructions
+
+### Subsection with detailed steps
+
+1. First step
+2. Second step
+3. Third step
+
+## Code Examples
+
+[Multi-language tabs - see below]
+
+## Next Steps
+
+- Link to related guide 1
+- Link to related guide 2
+```
+
+### 3. Multi-Language Code Examples
+
+**IMPORTANT:** ALL code examples in the documentation must include examples in ALL supported Seam SDK languages plus cURL.
+
+**Pattern:** Use GitBook tabs for showing code in multiple languages
+
+````markdown
+{% tabs %}
+{% tab title="Python" %}
+
+```python
+seam.access_codes.create(
+ device_id="...",
+ code="1234"
+)
+```
+````
+
+{% endtab %}
+
+{% tab title="JavaScript" %}
+
+```javascript
+await seam.accessCodes.create({
+ deviceId: '...',
+ code: '1234',
+})
+```
+
+{% endtab %}
+
+{% tab title="Ruby" %}
+
+```ruby
+seam.access_codes.create(
+ device_id: "...",
+ code: "1234"
+)
+```
+
+{% endtab %}
+
+{% tab title="PHP" %}
+
+```php
+access_codes->create(
+ device_id: "...",
+ code: "1234"
+);
+```
+
+{% endtab %}
+
+{% tab title="C#" %}
+
+```csharp
+seam.AccessCodes.Create(
+ deviceId: "...",
+ code: "1234"
+);
+```
+
+{% endtab %}
+
+{% tab title="Java" %}
+
+```java
+seam.accessCodes().create(
+ AccessCodesCreateRequest.builder()
+ .deviceId("...")
+ .code("1234")
+ .build()
+);
+```
+
+{% endtab %}
+
+{% tab title="cURL (bash)" %}
+
+```bash
+curl -X 'POST' \
+ 'https://connect.getseam.com/access_codes/create' \
+ -H "Authorization: Bearer ${SEAM_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -d '{"device_id": "...", "code": "1234"}'
+```
+
+{% endtab %}
+{% endtabs %}
+
+````
+
+**Required languages (in this order):**
+1. **Python** - Always first
+2. **JavaScript** - Second
+3. **Ruby** - Third
+4. **PHP** - Fourth
+5. **C#** - Fifth
+6. **Java** - Sixth
+7. **cURL (bash)** - Always last for direct API access
+
+**When to include code examples:**
+- ✅ All API operations and method calls
+- ✅ Complete workflows and multi-step processes
+- ✅ Common operations in setup guides
+- ✅ Any code that developers would copy and use
+- ❌ Simple conceptual explanations without implementation
+- ❌ Reference-only sections (but include in API docs)
+
+### 4. Code Example Conventions
+
+**Pattern:** Get resource → Check capability → Perform action
+
+```python
+# Get the device
+device = seam.devices.get(device_id="...")
+
+# Check capability
+if device.can_program_online_access_codes:
+ # Perform action
+ access_code = seam.access_codes.create(
+ device_id=device.device_id,
+ code="1234"
+ )
+````
+
+**Best practices:**
+
+- Always check capabilities before actions
+- Use descriptive variable names
+- Include inline comments for clarity
+- Show both request and response
+- Use consistent sample IDs across examples
+
+### 5. Navigation Patterns
+
+#### Card-Based Navigation
+
+Used for landing pages and section overviews:
+
+```markdown
+
+```
+
+#### Next Steps Sections
+
+End most guides with actionable next steps:
+
+```markdown
+## Next Steps
+
+- [Related Guide 1](../path/to/guide1.md)
+- [Related Guide 2](../path/to/guide2.md)
+- [API Reference](../../api/endpoint.md)
+```
+
+### 6. Callout Boxes (GitBook Hints)
+
+**Success hints** (tips, alternatives, helpful info):
+
+```markdown
+{% hint style="success" %}
+You can also use the Seam CLI to create access codes quickly during development.
+{% endhint %}
+```
+
+**Warning hints** (important caveats, deprecated features):
+
+```markdown
+{% hint style="warning" %}
+This feature is deprecated. Use the new Access Grants API instead.
+{% endhint %}
+```
+
+**Info hints** (additional context, sandbox notes):
+
+```markdown
+{% hint style="info" %}
+In the sandbox environment, access codes are created instantly without delay.
+{% endhint %}
+```
+
+### 7. Visual Content
+
+#### Images with Dark/Light Mode Support
+
+```markdown
+
+
+
+
+
+ Caption explaining the diagram
+
+```
+
+#### Embedded Videos
+
+```markdown
+{% embed url="https://www.youtube.com/watch?v=..." %}
+Brief description of video content
+{% endembed %}
+```
+
+#### Diagrams
+
+- Timeline diagrams for lifecycle events
+- State machine diagrams for access codes
+- Architecture diagrams for system overviews
+- Flow diagrams for multi-step processes
+
+### 8. Cross-References
+
+**Internal links:** Use relative paths from current file
+
+```markdown
+See the [Access Codes API Reference](../../api/access_codes/create.md) for details.
+```
+
+**External links:**
+
+- Seam Console: Link to specific console pages
+- GitHub: Link to SDK repositories
+- Package managers: Direct install links
+
+**Related content sections:**
+
+```markdown
+## Related Resources
+
+- [Core Concept: Devices](../../core-concepts/devices/)
+- [Capability Guide: Smart Locks](../../capability-guides/smart-locks/)
+- [API: devices.get](../../api/devices/get.md)
+```
+
+### 9. Tables
+
+**Parameter tables:**
+
+```markdown
+| Parameter | Type | Required | Description |
+| ----------- | ------ | -------- | ------------------------------- |
+| `device_id` | string | Yes | ID of the device |
+| `code` | string | No | Custom access code (4-8 digits) |
+```
+
+**Comparison tables:**
+
+```markdown
+| Feature | Basic Plan | Pro Plan |
+| ------------- | ---------- | -------- |
+| Access codes | ✓ | ✓ |
+| Mobile access | - | ✓ |
+```
+
+### 10. Prerequisites Sections
+
+**Standard format:**
+
+```markdown
+## Before You Begin
+
+To follow this guide, you need:
+
+- A Seam account (create one at [console.seam.co](https://console.seam.co))
+- An API key (found in your workspace settings)
+- A connected device (see [Getting Started](../quickstart.md))
+- Seam SDK installed (see [Installation](../api/installation.md))
+```
+
+---
+
+## Key Guidelines
+
+### For Working with This Repository
+
+1. **Auto-generated content:** The `/docs/api/` directory appears to be auto-generated. Avoid manual edits here - changes should be made to the source spec.
+
+2. **Consistency is critical:** With 627 files, maintaining consistent formatting, tone, and structure is essential. Follow existing patterns exactly.
+
+3. **GitBook-specific syntax:** This documentation uses GitBook markdown extensions (tabs, hints, cards, embeds). Standard markdown won't render these correctly.
+
+4. **Multi-language support:** All code examples should be provided in at least Python, JavaScript, and cURL. Additional languages (Ruby, PHP, C#, Java) are preferred.
+
+5. **Progressive disclosure:** Documentation flows from simple (quickstart) to complex (detailed guides). Maintain this hierarchy when adding content.
+
+6. **Capability-driven organization:** Features are organized by what devices CAN do, not just what they ARE. This is a key architectural principle.
+
+7. **Real-world focus:** Include sandbox credentials, troubleshooting guides, and production checklists. Developers need practical, actionable information.
+
+8. **Link maintenance:** When restructuring, always add redirects in `.gitbook.yaml` to maintain backward compatibility.
+
+9. **Visual aids:** Use screenshots, diagrams, and videos generously. IoT integrations benefit from visual guidance.
+
+10. **Accessibility:** Provide alt text for images, use semantic HTML, and ensure code examples are properly formatted.
+
+### Writing Checklist
+
+When creating or updating documentation:
+
+- [ ] Add descriptive frontmatter
+- [ ] Include code examples in multiple languages
+- [ ] Add "Before You Begin" prerequisites section
+- [ ] Check capability flags before actions in code examples
+- [ ] Include "Next Steps" with related links
+- [ ] Add images with dark/light mode support if applicable
+- [ ] Use appropriate hint boxes for tips, warnings, and info
+- [ ] Verify all internal links use relative paths
+- [ ] Follow standard heading hierarchy (H1 → H2 → H3)
+- [ ] Keep paragraphs short (2-3 sentences)
+- [ ] Use active voice and second person
+- [ ] Add entry to SUMMARY.md if creating new page
+- [ ] Update .gitbook.yaml redirects if restructuring
+
+### Common Terminology
+
+**Preferred terms:**
+
+- "Device" (not "smart lock" generically)
+- "Access code" (not "PIN code")
+- "Connected account" (not "integration" or "connection")
+- "Workspace" (not "account" or "organization")
+- "Capability" (not "feature" or "function")
+- "Provider" (not "brand" or "manufacturer" in technical contexts)
+
+### Tone Examples
+
+**Good:**
+
+> "Create an access code by calling the `access_codes.create` endpoint. You'll need to provide the `device_id` and optionally specify a custom code."
+
+**Avoid:**
+
+> "The access code creation functionality can be accessed through the utilization of the access codes creation API endpoint, which requires authentication and accepts various parameters."
+
+**Good:**
+
+> "The device must support online access code programming. Check the `can_program_online_access_codes` capability before proceeding."
+
+**Avoid:**
+
+> "Please note that it is important to verify whether or not the device has the necessary capabilities to support the programming of online access codes prior to attempting to create one."
+
+---
+
+## Summary
+
+The Seam API documentation is a mature, well-structured system designed for a developer audience building IoT integrations. Key characteristics:
+
+- **Developer-first:** Heavy emphasis on code examples and SDKs
+- **Capability-driven:** Organized by what devices can DO
+- **Multi-language:** Comprehensive SDK coverage (7+ languages)
+- **Production-ready:** Includes sandbox environments, troubleshooting, and go-live checklists
+- **Manufacturer-agnostic:** Provides unified API across 40+ device brands
+- **GitBook-optimized:** Extensive use of GitBook-specific markdown features
+
+When contributing to this documentation, prioritize consistency, clarity, and practical examples. The documentation serves developers who need to integrate Seam quickly and reliably into production applications.
diff --git a/docs/.gitbook/assets/ultraloq-logo.png b/docs/.gitbook/assets/ultraloq-logo.png
new file mode 100644
index 000000000..7f994c7e3
Binary files /dev/null and b/docs/.gitbook/assets/ultraloq-logo.png differ
diff --git a/docs/.gitbook/assets/ultraloq-manufacturer-page-cover-dark.png b/docs/.gitbook/assets/ultraloq-manufacturer-page-cover-dark.png
new file mode 100644
index 000000000..e74f097dd
Binary files /dev/null and b/docs/.gitbook/assets/ultraloq-manufacturer-page-cover-dark.png differ
diff --git a/docs/.gitbook/assets/ultraloq-manufacturer-page-cover-light.png b/docs/.gitbook/assets/ultraloq-manufacturer-page-cover-light.png
new file mode 100644
index 000000000..ba36b6f83
Binary files /dev/null and b/docs/.gitbook/assets/ultraloq-manufacturer-page-cover-light.png differ
diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md
index 7a73bef88..b96ee2c91 100644
--- a/docs/SUMMARY.md
+++ b/docs/SUMMARY.md
@@ -602,6 +602,10 @@
* [Get Started with Tedee Locks](device-and-system-integration-guides/tedee-locks/get-started-with-tedee-locks.md)
* [TTLock Locks](device-guides/ttlock-locks.md)
* [Get started with TTLock Locks](device-guides/get-started-with-ttlock-devices.md)
+* [Ultraloq Locks](device-and-system-integration-guides/ultraloq-locks/README.md)
+ * [Ultraloq Setup Guide](device-and-system-integration-guides/ultraloq-locks/ultraloq-setup-guide.md)
+ * [Configuring Ultraloq Device Timezones](device-and-system-integration-guides/ultraloq-locks/configuring-ultraloq-device-timezones.md)
+ * [Creating Ultraloq Access Codes](device-and-system-integration-guides/ultraloq-locks/creating-ultraloq-access-codes.md)
* [Wyze Locks](device-guides/wyze-locks.md)
* [Get started with Wyze Locks](device-guides/get-started-with-wyze-locks.md)
* [Yale Locks](device-guides/yale-locks.md)
diff --git a/docs/capability-guides/smart-locks/access-codes/creating-access-codes/understanding-code-constraints.md b/docs/capability-guides/smart-locks/access-codes/creating-access-codes/understanding-code-constraints.md
index d024df98b..50473c049 100644
--- a/docs/capability-guides/smart-locks/access-codes/creating-access-codes/understanding-code-constraints.md
+++ b/docs/capability-guides/smart-locks/access-codes/creating-access-codes/understanding-code-constraints.md
@@ -9,3 +9,178 @@ When creating access codes, it is important to be aware of any constraints on th
The `constraint_type` property can be any of the following enum values:
Constraint Type Description no_zerosCannot use 0s as digits in the PIN code. cannot_start_with_12PIN code cannot start with the sequence of digits 12. no_triple_consecutive_intsNo more than three digits in a row can be consecutive or the same in the PIN code. cannot_specify_pin_codeCannot specify a PIN code. You must leave the code empty, and the lock provider generates a PIN code. pin_code_matches_existing_setIf you specify a PIN code, it must match an existing set of PIN codes used in the account.
For example, the PIN code could match the code assigned to a user in the system.
start_date_in_futureFor time-bound codes, the start date must be in the future. no_ascending_or_descending_sequencePIN code cannot consist of a sequence of consecutive digits. at_least_three_unique_digitsPIN code must contain at least three unique digits. cannot_contain_089PIN code cannot contain the digits 0, 8, or 9.
For example, this restriction could apply to a cylinder lock that only includes the digits 1 to 7.
cannot_contain_0789PIN code cannot contain the digits 0, 7, 8, or 9.
For example, this restriction could apply to a cylinder lock that only includes the digits 1 to 6.
name_lengthName of the code has some restrictions on length.
When the constraint_type is name_length, the constraint object has one or two additional properties called min_length and max_length to specify the length constraints.
name_must_be_uniqueName of the code must be unique within the device.
+
+***
+
+## Provider-Specific Requirements
+
+In addition to code constraints, some device providers have additional requirements for creating access codes.
+
+### Timezone Configuration
+
+Some device providers require you to configure the device's timezone before creating time-bound access codes. This is because these devices schedule access codes using device-local time, but their APIs do not report the device's timezone.
+
+**Providers requiring timezone configuration:**
+
+* **Ultraloq** — Must configure timezone using `/devices/report_provider_metadata` before creating time-bound access codes. See [Configuring Ultraloq Device Timezones](../../../../device-and-system-integration-guides/ultraloq-locks/configuring-ultraloq-device-timezones.md).
+
+{% hint style="info" %}
+Permanent access codes (codes without `starts_at` and `ends_at`) do not require timezone configuration, even on providers that require it for time-bound codes.
+{% endhint %}
+
+**Detecting timezone requirement:**
+
+Check for the provider-specific timezone warning in `device.warnings`:
+
+{% tabs %}
+{% tab title="Python" %}
+```python
+from seam import Seam
+
+seam = Seam()
+
+device = seam.devices.get(device_id="your-device-id")
+
+# Check for timezone warnings
+timezone_warnings = [
+ w for w in device.warnings
+ if "time_zone" in w.warning_code.lower()
+]
+
+if timezone_warnings:
+ print("⚠️ Timezone configuration required for time-bound codes")
+ print(f"Warning: {timezone_warnings[0].message}")
+```
+{% endtab %}
+
+{% tab title="JavaScript" %}
+```javascript
+import { Seam } from "seam";
+
+const seam = new Seam();
+
+const device = await seam.devices.get({
+ device_id: "your-device-id"
+});
+
+// Check for timezone warnings
+const timezoneWarnings = device.warnings.filter(
+ w => w.warning_code.toLowerCase().includes("time_zone")
+);
+
+if (timezoneWarnings.length > 0) {
+ console.log("⚠️ Timezone configuration required for time-bound codes");
+ console.log(`Warning: ${timezoneWarnings[0].message}`);
+}
+```
+{% endtab %}
+
+{% tab title="Ruby" %}
+```ruby
+require "seam"
+
+seam = Seam.new()
+
+device = seam.devices.get(device_id: "your-device-id")
+
+# Check for timezone warnings
+timezone_warnings = device.warnings.select do |w|
+ w.warning_code.downcase.include?("time_zone")
+end
+
+if timezone_warnings.any?
+ puts "⚠️ Timezone configuration required for time-bound codes"
+ puts "Warning: #{timezone_warnings[0].message}"
+end
+```
+{% endtab %}
+
+{% tab title="PHP" %}
+```php
+devices->get(device_id: "your-device-id");
+
+// Check for timezone warnings
+$timezoneWarnings = array_filter($device->warnings, function($w) {
+ return stripos($w->warning_code, "time_zone") !== false;
+});
+
+if (count($timezoneWarnings) > 0) {
+ echo "⚠️ Timezone configuration required for time-bound codes\n";
+ echo "Warning: " . array_values($timezoneWarnings)[0]->message . "\n";
+}
+```
+{% endtab %}
+
+{% tab title="C#" %}
+```csharp
+using Seam.Client;
+using System.Linq;
+
+var seam = new SeamClient();
+
+var device = seam.Devices.Get(deviceId: "your-device-id");
+
+// Check for timezone warnings
+var timezoneWarnings = device.Warnings
+ .Where(w => w.WarningCode.ToLower().Contains("time_zone"))
+ .ToList();
+
+if (timezoneWarnings.Any())
+{
+ Console.WriteLine("⚠️ Timezone configuration required for time-bound codes");
+ Console.WriteLine($"Warning: {timezoneWarnings[0].Message}");
+}
+```
+{% endtab %}
+
+{% tab title="Java" %}
+```java
+import com.seam.api.Seam;
+import com.seam.api.types.Device;
+import java.util.stream.Collectors;
+
+Seam seam = Seam.builder().build();
+
+Device device = seam.devices().get(
+ DevicesGetRequest.builder()
+ .deviceId("your-device-id")
+ .build()
+);
+
+// Check for timezone warnings
+var timezoneWarnings = device.getWarnings().stream()
+ .filter(w -> w.getWarningCode().toLowerCase().contains("time_zone"))
+ .collect(Collectors.toList());
+
+if (!timezoneWarnings.isEmpty()) {
+ System.out.println("⚠️ Timezone configuration required for time-bound codes");
+ System.out.println("Warning: " + timezoneWarnings.get(0).getMessage());
+}
+```
+{% endtab %}
+
+{% tab title="cURL (bash)" %}
+```bash
+device=$(curl -X 'POST' \
+ 'https://connect.getseam.com/devices/get' \
+ -H 'accept: application/json' \
+ -H "Authorization: Bearer ${SEAM_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -d "{
+ \"device_id\": \"your-device-id\"
+ }")
+
+# Check for timezone warnings
+echo $device | jq '.device.warnings[] | select(.warning_code | test("time_zone"; "i"))'
+```
+{% endtab %}
+{% endtabs %}
+
+For devices requiring timezone configuration, attempting to create a time-bound access code without first setting the timezone will result in a validation error.
diff --git a/docs/device-and-system-integration-guides/ultraloq-locks/README.md b/docs/device-and-system-integration-guides/ultraloq-locks/README.md
new file mode 100644
index 000000000..ac0d1fe93
--- /dev/null
+++ b/docs/device-and-system-integration-guides/ultraloq-locks/README.md
@@ -0,0 +1,98 @@
+---
+description: Guide for using Ultraloq smart locks with Seam
+---
+
+# Ultraloq Locks
+
+Connect and control Ultraloq devices using the Seam API.
+
+## Overview
+
+Seam integrates with Ultraloq smart locks, providing Wi-Fi-enabled access control with online programming capabilities. Ultraloq locks support both permanent and time-bound access codes, remote lock and unlock operations, and device monitoring.
+
+{% hint style="warning" %}
+**Important:** Ultraloq devices require timezone configuration before you can create time-bound access codes. This is a unique requirement for Ultraloq locks. See [Configuring Ultraloq Device Timezones](configuring-ultraloq-device-timezones.md) for details.
+{% endhint %}
+
+***
+
+## Supported Devices
+
+All Ultraloq smart locks with Wi-Fi connectivity are supported through this integration.
+
+For detailed information about the Ultraloq devices that Seam supports, see our [Ultraloq Supported Devices page](https://www.seam.co/manufacturers/ultraloq).
+
+***
+
+## Supported Features
+
+We support the following features:
+
+#### Device control
+
+* Lock and unlock actions (online)
+
+#### Access code management
+
+* Permanent access codes (no timezone required)
+* Time-bound access codes (requires timezone configuration)
+* Custom code lengths between 4 and 8 digits
+* Auto-generated codes
+
+#### Device monitoring
+
+* Lock status
+* Online/offline state
+* Battery level (where supported)
+
+***
+
+## Time Zone Requirement
+
+Unlike most other integrations, Ultraloq devices require timezone configuration to enable time-bound access codes. This is because Ultraloq devices schedule access codes using device-local time, but the Ultraloq API does not report the device's timezone.
+
+### What Works Without Timezone
+
+* ✅ Permanent access codes (codes without start/end times)
+* ✅ Lock and unlock operations
+* ✅ Device monitoring
+
+### What Requires Timezone
+
+* ❌ Time-bound access codes (codes with `starts_at` and `ends_at`)
+
+When you first connect an Ultraloq device, it will have a `ultraloq_time_zone_unknown` warning in `device.warnings`. You must configure the timezone using the `/devices/report_provider_metadata` endpoint before creating time-bound access codes.
+
+For complete instructions, see [Configuring Ultraloq Device Timezones](configuring-ultraloq-device-timezones.md).
+
+***
+
+## Connecting Ultraloq to Seam
+
+To enable your users to [connect Ultraloq devices through Connect Webviews](../../core-concepts/connect-webviews/customizing-connect-webviews.md#customize-the-brands-to-display-in-your-connect-webviews), include the Ultraloq provider:
+
+```json
+{
+ "accepted_providers": ["ultraloq"]
+}
+```
+
+After the user authorizes Seam through the OAuth flow, their Ultraloq devices will be automatically discovered and added to Seam.
+
+[→ See: Ultraloq Setup Guide](ultraloq-setup-guide.md)
+
+***
+
+## Brand-specific notes
+
+* **Access codes:** Ultraloq requires access codes to be 4–8 digit numeric PINs (e.g., "1234", "567890").
+* **Timezone configuration:** Required before creating time-bound access codes. Permanent codes work without timezone configuration.
+* **Code disabling:** Users can disable access codes through the Ultraloq mobile app. Seam detects this and adds a `ultraloq_access_code_disabled` warning to the affected access code.
+
+***
+
+## Next Steps
+
+
+
+***
diff --git a/docs/device-and-system-integration-guides/ultraloq-locks/configuring-ultraloq-device-timezones.md b/docs/device-and-system-integration-guides/ultraloq-locks/configuring-ultraloq-device-timezones.md
new file mode 100644
index 000000000..8581ec536
--- /dev/null
+++ b/docs/device-and-system-integration-guides/ultraloq-locks/configuring-ultraloq-device-timezones.md
@@ -0,0 +1,1199 @@
+---
+description: Configure device timezones for Ultraloq locks to enable time-bound access codes
+---
+
+# Configuring Ultraloq Device Timezones
+
+Ultraloq devices require timezone configuration before you can create time-bound access codes. This guide explains why this is necessary and how to configure timezones for your Ultraloq devices.
+
+***
+
+## Why Timezone Configuration is Required
+
+Ultraloq devices have a unique characteristic that requires manual timezone configuration:
+
+**The Problem:**
+
+* Ultraloq devices schedule access codes using **device-local time** (e.g., "2024-01-15 14:30")
+* The Ultraloq API returns timestamps **without timezone information**
+* Without knowing the device's timezone, Seam cannot correctly convert UTC timestamps to device-local time
+
+**The Solution:**
+
+* You must manually configure each device's timezone using the `/devices/report_provider_metadata` API
+* This tells Seam what timezone the device is in so it can correctly schedule time-bound access codes
+
+{% hint style="info" %}
+This is a **one-time configuration** per device. Once set, the timezone persists until you change it.
+{% endhint %}
+
+***
+
+## What Works Without Timezone
+
+You can use the following features **without** configuring the device's timezone:
+
+* ✅ **Permanent access codes** — Codes without `starts_at` and `ends_at` work immediately
+* ✅ **Lock and unlock operations** — Remote lock/unlock commands work immediately
+* ✅ **Device monitoring** — Battery level, lock status, and online/offline state
+
+***
+
+## What Requires Timezone
+
+The following feature **requires** timezone configuration:
+
+* ❌ **Time-bound access codes** — Codes with `starts_at` and `ends_at` require timezone
+
+If you attempt to create a time-bound access code without configuring the timezone, you'll receive a validation error:
+
+```json
+{
+ "error": {
+ "type": "invalid_input",
+ "message": "Time zone required for time-bound access codes on Ultraloq devices"
+ }
+}
+```
+
+***
+
+## Detecting Unconfigured Devices
+
+When you first connect an Ultraloq device, it will have the `ultraloq_time_zone_unknown` warning:
+
+{% tabs %}
+{% tab title="Python" %}
+```python
+from seam import Seam
+
+seam = Seam()
+
+device = seam.devices.get(device_id="your-device-id")
+
+# Check for timezone warning
+has_timezone_warning = any(
+ w.warning_code == "ultraloq_time_zone_unknown"
+ for w in device.warnings
+)
+
+if has_timezone_warning:
+ print("⚠️ Timezone not configured")
+ print("Configure timezone before creating time-bound access codes")
+
+# Check timezone value
+timezone = device.properties.get("ultraloq_metadata", {}).get("time_zone")
+print(f"Current timezone: {timezone}") # Will be None if not configured
+```
+{% endtab %}
+
+{% tab title="JavaScript" %}
+```javascript
+import { Seam } from "seam";
+
+const seam = new Seam();
+
+const device = await seam.devices.get({
+ device_id: "your-device-id"
+});
+
+// Check for timezone warning
+const hasTimezoneWarning = device.warnings.some(
+ w => w.warning_code === "ultraloq_time_zone_unknown"
+);
+
+if (hasTimezoneWarning) {
+ console.log("⚠️ Timezone not configured");
+ console.log("Configure timezone before creating time-bound access codes");
+}
+
+// Check timezone value
+const timezone = device.properties.ultraloq_metadata?.time_zone;
+console.log(`Current timezone: ${timezone}`); // Will be null if not configured
+```
+{% endtab %}
+
+{% tab title="Ruby" %}
+```ruby
+require "seam"
+
+seam = Seam.new()
+
+device = seam.devices.get(device_id: "your-device-id")
+
+# Check for timezone warning
+has_timezone_warning = device.warnings.any? do |w|
+ w.warning_code == "ultraloq_time_zone_unknown"
+end
+
+if has_timezone_warning
+ puts "⚠️ Timezone not configured"
+ puts "Configure timezone before creating time-bound access codes"
+end
+
+# Check timezone value
+timezone = device.properties.dig("ultraloq_metadata", "time_zone")
+puts "Current timezone: #{timezone}" # Will be nil if not configured
+```
+{% endtab %}
+
+{% tab title="PHP" %}
+```php
+devices->get(device_id: "your-device-id");
+
+// Check for timezone warning
+$hasTimezoneWarning = false;
+foreach ($device->warnings as $warning) {
+ if ($warning->warning_code === "ultraloq_time_zone_unknown") {
+ $hasTimezoneWarning = true;
+ break;
+ }
+}
+
+if ($hasTimezoneWarning) {
+ echo "⚠️ Timezone not configured\n";
+ echo "Configure timezone before creating time-bound access codes\n";
+}
+
+// Check timezone value
+$timezone = $device->properties->ultraloq_metadata->time_zone ?? null;
+echo "Current timezone: " . ($timezone ?? "not set") . "\n";
+```
+{% endtab %}
+
+{% tab title="C#" %}
+```csharp
+using Seam.Client;
+
+var seam = new SeamClient();
+
+var device = seam.Devices.Get(deviceId: "your-device-id");
+
+// Check for timezone warning
+var hasTimezoneWarning = device.Warnings.Any(
+ w => w.WarningCode == "ultraloq_time_zone_unknown"
+);
+
+if (hasTimezoneWarning)
+{
+ Console.WriteLine("⚠️ Timezone not configured");
+ Console.WriteLine("Configure timezone before creating time-bound access codes");
+}
+
+// Check timezone value
+var timezone = device.Properties.UltraloqMetadata?.TimeZone;
+Console.WriteLine($"Current timezone: {timezone ?? "not set"}");
+```
+{% endtab %}
+
+{% tab title="Java" %}
+```java
+import com.seam.api.Seam;
+import com.seam.api.types.Device;
+
+Seam seam = Seam.builder().build();
+
+Device device = seam.devices().get(
+ DevicesGetRequest.builder()
+ .deviceId("your-device-id")
+ .build()
+);
+
+// Check for timezone warning
+boolean hasTimezoneWarning = device.getWarnings().stream()
+ .anyMatch(w -> w.getWarningCode().equals("ultraloq_time_zone_unknown"));
+
+if (hasTimezoneWarning) {
+ System.out.println("⚠️ Timezone not configured");
+ System.out.println("Configure timezone before creating time-bound access codes");
+}
+
+// Check timezone value
+String timezone = device.getProperties().getUltraloqMetadata().getTimeZone();
+System.out.println("Current timezone: " + (timezone != null ? timezone : "not set"));
+```
+{% endtab %}
+
+{% tab title="cURL (bash)" %}
+```bash
+device=$(curl -X 'POST' \
+ 'https://connect.getseam.com/devices/get' \
+ -H 'accept: application/json' \
+ -H "Authorization: Bearer ${SEAM_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -d "{
+ \"device_id\": \"your-device-id\"
+ }")
+
+# Check for timezone warning
+echo $device | jq '.device.warnings[] | select(.warning_code == "ultraloq_time_zone_unknown")'
+
+# Check timezone value
+echo $device | jq '.device.properties.ultraloq_metadata.time_zone'
+```
+{% endtab %}
+{% endtabs %}
+
+***
+
+## Configuring Timezones
+
+### Single Device
+
+To configure the timezone for a single device, use the `/devices/report_provider_metadata` endpoint:
+
+{% tabs %}
+{% tab title="Python" %}
+```python
+from seam import Seam
+
+seam = Seam()
+
+# Configure timezone for one device
+seam.devices.report_provider_metadata(
+ devices=[
+ {
+ "device_id": "your-device-id",
+ "ultraloq_metadata": {
+ "time_zone": "America/New_York"
+ }
+ }
+ ]
+)
+
+print("✓ Timezone configured successfully!")
+```
+{% endtab %}
+
+{% tab title="JavaScript" %}
+```javascript
+import { Seam } from "seam";
+
+const seam = new Seam();
+
+// Configure timezone for one device
+await seam.devices.reportProviderMetadata({
+ devices: [
+ {
+ device_id: "your-device-id",
+ ultraloq_metadata: {
+ time_zone: "America/New_York"
+ }
+ }
+ ]
+});
+
+console.log("✓ Timezone configured successfully!");
+```
+{% endtab %}
+
+{% tab title="Ruby" %}
+```ruby
+require "seam"
+
+seam = Seam.new()
+
+# Configure timezone for one device
+seam.devices.report_provider_metadata(
+ devices: [
+ {
+ device_id: "your-device-id",
+ ultraloq_metadata: {
+ time_zone: "America/New_York"
+ }
+ }
+ ]
+)
+
+puts "✓ Timezone configured successfully!"
+```
+{% endtab %}
+
+{% tab title="PHP" %}
+```php
+devices->report_provider_metadata(
+ devices: [
+ [
+ "device_id" => "your-device-id",
+ "ultraloq_metadata" => [
+ "time_zone" => "America/New_York"
+ ]
+ ]
+ ]
+);
+
+echo "✓ Timezone configured successfully!";
+```
+{% endtab %}
+
+{% tab title="C#" %}
+```csharp
+using Seam.Client;
+
+var seam = new SeamClient();
+
+// Configure timezone for one device
+seam.Devices.ReportProviderMetadata(
+ devices: new[] {
+ new DeviceMetadata {
+ DeviceId = "your-device-id",
+ UltraloqMetadata = new UltraloqMetadata {
+ TimeZone = "America/New_York"
+ }
+ }
+ }
+);
+
+Console.WriteLine("✓ Timezone configured successfully!");
+```
+{% endtab %}
+
+{% tab title="Java" %}
+```java
+import com.seam.api.Seam;
+
+Seam seam = Seam.builder().build();
+
+// Configure timezone for one device
+seam.devices().reportProviderMetadata(
+ DevicesReportProviderMetadataRequest.builder()
+ .devices(List.of(
+ DeviceMetadata.builder()
+ .deviceId("your-device-id")
+ .ultraloqMetadata(UltraloqMetadata.builder()
+ .timeZone("America/New_York")
+ .build())
+ .build()
+ ))
+ .build()
+);
+
+System.out.println("✓ Timezone configured successfully!");
+```
+{% endtab %}
+
+{% tab title="cURL (bash)" %}
+```bash
+curl -X 'POST' \
+ 'https://connect.getseam.com/devices/report_provider_metadata' \
+ -H 'accept: application/json' \
+ -H "Authorization: Bearer ${SEAM_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "devices": [
+ {
+ "device_id": "your-device-id",
+ "ultraloq_metadata": {
+ "time_zone": "America/New_York"
+ }
+ }
+ ]
+ }'
+```
+{% endtab %}
+{% endtabs %}
+
+### Multiple Devices (Batch Configuration)
+
+You can configure timezones for multiple devices in a single API call:
+
+{% tabs %}
+{% tab title="Python" %}
+```python
+from seam import Seam
+
+seam = Seam()
+
+# Get all Ultraloq devices
+devices = seam.devices.list(device_type="ultraloq_lock")
+
+# Configure timezone for all devices
+seam.devices.report_provider_metadata(
+ devices=[
+ {
+ "device_id": device.device_id,
+ "ultraloq_metadata": {
+ "time_zone": "America/Los_Angeles" # Or get from user
+ }
+ }
+ for device in devices
+ ]
+)
+
+print(f"✓ Configured timezone for {len(devices)} devices")
+```
+{% endtab %}
+
+{% tab title="JavaScript" %}
+```javascript
+import { Seam } from "seam";
+
+const seam = new Seam();
+
+// Get all Ultraloq devices
+const devices = await seam.devices.list({
+ device_type: "ultraloq_lock"
+});
+
+// Configure timezone for all devices
+await seam.devices.reportProviderMetadata({
+ devices: devices.map(device => ({
+ device_id: device.device_id,
+ ultraloq_metadata: {
+ time_zone: "America/Los_Angeles" // Or get from user
+ }
+ }))
+});
+
+console.log(`✓ Configured timezone for ${devices.length} devices`);
+```
+{% endtab %}
+
+{% tab title="Ruby" %}
+```ruby
+require "seam"
+
+seam = Seam.new()
+
+# Get all Ultraloq devices
+devices = seam.devices.list(device_type: "ultraloq_lock")
+
+# Configure timezone for all devices
+seam.devices.report_provider_metadata(
+ devices: devices.map do |device|
+ {
+ device_id: device.device_id,
+ ultraloq_metadata: {
+ time_zone: "America/Los_Angeles" # Or get from user
+ }
+ }
+ end
+)
+
+puts "✓ Configured timezone for #{devices.length} devices"
+```
+{% endtab %}
+
+{% tab title="PHP" %}
+```php
+devices->list(device_type: "ultraloq_lock");
+
+// Configure timezone for all devices
+$deviceMetadata = array_map(function($device) {
+ return [
+ "device_id" => $device->device_id,
+ "ultraloq_metadata" => [
+ "time_zone" => "America/Los_Angeles" // Or get from user
+ ]
+ ];
+}, $devices);
+
+$seam->devices->report_provider_metadata(devices: $deviceMetadata);
+
+echo "✓ Configured timezone for " . count($devices) . " devices";
+```
+{% endtab %}
+
+{% tab title="C#" %}
+```csharp
+using Seam.Client;
+using System.Linq;
+
+var seam = new SeamClient();
+
+// Get all Ultraloq devices
+var devices = seam.Devices.List(deviceType: "ultraloq_lock");
+
+// Configure timezone for all devices
+seam.Devices.ReportProviderMetadata(
+ devices: devices.Select(device => new DeviceMetadata {
+ DeviceId = device.DeviceId,
+ UltraloqMetadata = new UltraloqMetadata {
+ TimeZone = "America/Los_Angeles" // Or get from user
+ }
+ }).ToArray()
+);
+
+Console.WriteLine($"✓ Configured timezone for {devices.Count()} devices");
+```
+{% endtab %}
+
+{% tab title="Java" %}
+```java
+import com.seam.api.Seam;
+import com.seam.api.types.Device;
+import java.util.stream.Collectors;
+
+Seam seam = Seam.builder().build();
+
+// Get all Ultraloq devices
+List devices = seam.devices().list(
+ DevicesListRequest.builder()
+ .deviceType("ultraloq_lock")
+ .build()
+);
+
+// Configure timezone for all devices
+seam.devices().reportProviderMetadata(
+ DevicesReportProviderMetadataRequest.builder()
+ .devices(devices.stream()
+ .map(device -> DeviceMetadata.builder()
+ .deviceId(device.getDeviceId())
+ .ultraloqMetadata(UltraloqMetadata.builder()
+ .timeZone("America/Los_Angeles") // Or get from user
+ .build())
+ .build())
+ .collect(Collectors.toList()))
+ .build()
+);
+
+System.out.println("✓ Configured timezone for " + devices.size() + " devices");
+```
+{% endtab %}
+
+{% tab title="cURL (bash)" %}
+```bash
+# Get all Ultraloq devices
+devices=$(curl -X 'POST' \
+ 'https://connect.getseam.com/devices/list' \
+ -H 'accept: application/json' \
+ -H "Authorization: Bearer ${SEAM_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "device_type": "ultraloq_lock"
+ }')
+
+# Configure timezone for all devices
+# (Requires jq to construct the request)
+curl -X 'POST' \
+ 'https://connect.getseam.com/devices/report_provider_metadata' \
+ -H 'accept: application/json' \
+ -H "Authorization: Bearer ${SEAM_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -d "$(echo $devices | jq '{
+ devices: [.devices[] | {
+ device_id: .device_id,
+ ultraloq_metadata: {
+ time_zone: "America/Los_Angeles"
+ }
+ }]
+ }')"
+```
+{% endtab %}
+{% endtabs %}
+
+***
+
+## Valid Timezone Values
+
+You must use **IANA timezone strings** (also called "tz database" timezones). These are standardized timezone identifiers in the format `Continent/City`.
+
+### Examples of Valid Timezones
+
+* `"America/New_York"` — Eastern Time (US)
+* `"America/Chicago"` — Central Time (US)
+* `"America/Denver"` — Mountain Time (US)
+* `"America/Los_Angeles"` — Pacific Time (US)
+* `"America/Phoenix"` — Arizona (no DST)
+* `"America/Toronto"` — Eastern Time (Canada)
+* `"Europe/London"` — UK
+* `"Europe/Paris"` — Central European Time
+* `"Asia/Tokyo"` — Japan
+* `"Australia/Sydney"` — Australian Eastern Time
+
+{% hint style="warning" %}
+**Do not use timezone abbreviations** like `"EST"`, `"PST"`, or `"GMT-5"`. These are ambiguous and will cause validation errors. Always use the full IANA timezone string.
+{% endhint %}
+
+### Finding the Right Timezone
+
+For a complete list of valid IANA timezones, see:
+
+* [IANA Time Zone Database (Wikipedia)](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
+* [IANA Official Database](https://www.iana.org/time-zones)
+
+Most programming languages also provide timezone lookup utilities:
+
+{% tabs %}
+{% tab title="Python" %}
+```python
+import pytz
+
+# List all available timezones
+all_timezones = pytz.all_timezones
+print(f"Available timezones: {len(all_timezones)}")
+
+# Search for timezones containing "New"
+ny_timezones = [tz for tz in all_timezones if "New" in tz]
+print(ny_timezones)
+# ['America/New_York', 'America/North_Dakota/New_Salem', ...]
+```
+{% endtab %}
+
+{% tab title="JavaScript" %}
+```javascript
+// Using Intl API (built-in)
+const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+console.log(`User's timezone: ${userTimezone}`);
+// Example: "America/Los_Angeles"
+
+// Or using moment-timezone library
+const moment = require('moment-timezone');
+const allTimezones = moment.tz.names();
+console.log(`Available timezones: ${allTimezones.length}`);
+```
+{% endtab %}
+
+{% tab title="Ruby" %}
+```ruby
+require 'tzinfo'
+
+# List all available timezones
+all_timezones = TZInfo::Timezone.all_identifiers
+puts "Available timezones: #{all_timezones.length}"
+
+# Search for timezones containing "New"
+ny_timezones = all_timezones.select { |tz| tz.include?("New") }
+puts ny_timezones
+# ["America/New_York", "America/North_Dakota/New_Salem", ...]
+```
+{% endtab %}
+{% endtabs %}
+
+***
+
+## Verification
+
+After configuring the timezone, verify that the configuration was successful:
+
+{% tabs %}
+{% tab title="Python" %}
+```python
+from seam import Seam
+
+seam = Seam()
+
+device = seam.devices.get(device_id="your-device-id")
+
+# Verify timezone is set
+timezone = device.properties["ultraloq_metadata"]["time_zone"]
+print(f"Device timezone: {timezone}")
+
+# Verify warning is cleared
+has_warning = any(
+ w.warning_code == "ultraloq_time_zone_unknown"
+ for w in device.warnings
+)
+
+if not has_warning:
+ print("✓ Timezone configured successfully!")
+ print("✓ Device is ready to create time-bound access codes")
+else:
+ print("✗ Warning still present - check timezone configuration")
+```
+{% endtab %}
+
+{% tab title="JavaScript" %}
+```javascript
+import { Seam } from "seam";
+
+const seam = new Seam();
+
+const device = await seam.devices.get({
+ device_id: "your-device-id"
+});
+
+// Verify timezone is set
+const timezone = device.properties.ultraloq_metadata.time_zone;
+console.log(`Device timezone: ${timezone}`);
+
+// Verify warning is cleared
+const hasWarning = device.warnings.some(
+ w => w.warning_code === "ultraloq_time_zone_unknown"
+);
+
+if (!hasWarning) {
+ console.log("✓ Timezone configured successfully!");
+ console.log("✓ Device is ready to create time-bound access codes");
+} else {
+ console.log("✗ Warning still present - check timezone configuration");
+}
+```
+{% endtab %}
+
+{% tab title="Ruby" %}
+```ruby
+require "seam"
+
+seam = Seam.new()
+
+device = seam.devices.get(device_id: "your-device-id")
+
+# Verify timezone is set
+timezone = device.properties["ultraloq_metadata"]["time_zone"]
+puts "Device timezone: #{timezone}"
+
+# Verify warning is cleared
+has_warning = device.warnings.any? { |w| w.warning_code == "ultraloq_time_zone_unknown" }
+
+if !has_warning
+ puts "✓ Timezone configured successfully!"
+ puts "✓ Device is ready to create time-bound access codes"
+else
+ puts "✗ Warning still present - check timezone configuration"
+end
+```
+{% endtab %}
+
+{% tab title="PHP" %}
+```php
+devices->get(device_id: "your-device-id");
+
+// Verify timezone is set
+$timezone = $device->properties->ultraloq_metadata->time_zone;
+echo "Device timezone: $timezone\n";
+
+// Verify warning is cleared
+$hasWarning = false;
+foreach ($device->warnings as $warning) {
+ if ($warning->warning_code === "ultraloq_time_zone_unknown") {
+ $hasWarning = true;
+ break;
+ }
+}
+
+if (!$hasWarning) {
+ echo "✓ Timezone configured successfully!\n";
+ echo "✓ Device is ready to create time-bound access codes\n";
+} else {
+ echo "✗ Warning still present - check timezone configuration\n";
+}
+```
+{% endtab %}
+
+{% tab title="C#" %}
+```csharp
+using Seam.Client;
+
+var seam = new SeamClient();
+
+var device = seam.Devices.Get(deviceId: "your-device-id");
+
+// Verify timezone is set
+var timezone = device.Properties.UltraloqMetadata.TimeZone;
+Console.WriteLine($"Device timezone: {timezone}");
+
+// Verify warning is cleared
+var hasWarning = device.Warnings.Any(
+ w => w.WarningCode == "ultraloq_time_zone_unknown"
+);
+
+if (!hasWarning)
+{
+ Console.WriteLine("✓ Timezone configured successfully!");
+ Console.WriteLine("✓ Device is ready to create time-bound access codes");
+}
+else
+{
+ Console.WriteLine("✗ Warning still present - check timezone configuration");
+}
+```
+{% endtab %}
+
+{% tab title="Java" %}
+```java
+import com.seam.api.Seam;
+import com.seam.api.types.Device;
+
+Seam seam = Seam.builder().build();
+
+Device device = seam.devices().get(
+ DevicesGetRequest.builder()
+ .deviceId("your-device-id")
+ .build()
+);
+
+// Verify timezone is set
+String timezone = device.getProperties().getUltraloqMetadata().getTimeZone();
+System.out.println("Device timezone: " + timezone);
+
+// Verify warning is cleared
+boolean hasWarning = device.getWarnings().stream()
+ .anyMatch(w -> w.getWarningCode().equals("ultraloq_time_zone_unknown"));
+
+if (!hasWarning) {
+ System.out.println("✓ Timezone configured successfully!");
+ System.out.println("✓ Device is ready to create time-bound access codes");
+} else {
+ System.out.println("✗ Warning still present - check timezone configuration");
+}
+```
+{% endtab %}
+
+{% tab title="cURL (bash)" %}
+```bash
+device=$(curl -X 'POST' \
+ 'https://connect.getseam.com/devices/get' \
+ -H 'accept: application/json' \
+ -H "Authorization: Bearer ${SEAM_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -d "{
+ \"device_id\": \"your-device-id\"
+ }")
+
+# Check timezone
+echo $device | jq -r '.device.properties.ultraloq_metadata.time_zone'
+
+# Check warnings
+echo $device | jq '.device.warnings[] | select(.warning_code == "ultraloq_time_zone_unknown")'
+```
+{% endtab %}
+{% endtabs %}
+
+After configuration, the device will also have the timezone in `device.location.timezone`:
+
+```json
+{
+ "device_id": "...",
+ "properties": {
+ "ultraloq_metadata": {
+ "time_zone": "America/New_York"
+ }
+ },
+ "location": {
+ "timezone": "America/New_York"
+ },
+ "warnings": []
+}
+```
+
+***
+
+## Changing Timezones
+
+You can change a device's timezone at any time by calling `/devices/report_provider_metadata` again with the new timezone.
+
+{% tabs %}
+{% tab title="Python" %}
+```python
+from seam import Seam
+
+seam = Seam()
+
+# User moved device from New York to Los Angeles
+seam.devices.report_provider_metadata(
+ devices=[
+ {
+ "device_id": "your-device-id",
+ "ultraloq_metadata": {
+ "time_zone": "America/Los_Angeles" # Changed from America/New_York
+ }
+ }
+ ]
+)
+
+print("✓ Timezone updated to Pacific Time")
+```
+{% endtab %}
+
+{% tab title="JavaScript" %}
+```javascript
+import { Seam } from "seam";
+
+const seam = new Seam();
+
+// User moved device from New York to Los Angeles
+await seam.devices.reportProviderMetadata({
+ devices: [
+ {
+ device_id: "your-device-id",
+ ultraloq_metadata: {
+ time_zone: "America/Los_Angeles" // Changed from America/New_York
+ }
+ }
+ ]
+});
+
+console.log("✓ Timezone updated to Pacific Time");
+```
+{% endtab %}
+
+{% tab title="Ruby" %}
+```ruby
+require "seam"
+
+seam = Seam.new()
+
+# User moved device from New York to Los Angeles
+seam.devices.report_provider_metadata(
+ devices: [
+ {
+ device_id: "your-device-id",
+ ultraloq_metadata: {
+ time_zone: "America/Los_Angeles" # Changed from America/New_York
+ }
+ }
+ ]
+)
+
+puts "✓ Timezone updated to Pacific Time"
+```
+{% endtab %}
+
+{% tab title="PHP" %}
+```php
+devices->report_provider_metadata(
+ devices: [
+ [
+ "device_id" => "your-device-id",
+ "ultraloq_metadata" => [
+ "time_zone" => "America/Los_Angeles" // Changed from America/New_York
+ ]
+ ]
+ ]
+);
+
+echo "✓ Timezone updated to Pacific Time";
+```
+{% endtab %}
+
+{% tab title="C#" %}
+```csharp
+using Seam.Client;
+
+var seam = new SeamClient();
+
+// User moved device from New York to Los Angeles
+seam.Devices.ReportProviderMetadata(
+ devices: new[] {
+ new DeviceMetadata {
+ DeviceId = "your-device-id",
+ UltraloqMetadata = new UltraloqMetadata {
+ TimeZone = "America/Los_Angeles" // Changed from America/New_York
+ }
+ }
+ }
+);
+
+Console.WriteLine("✓ Timezone updated to Pacific Time");
+```
+{% endtab %}
+
+{% tab title="Java" %}
+```java
+import com.seam.api.Seam;
+
+Seam seam = Seam.builder().build();
+
+// User moved device from New York to Los Angeles
+seam.devices().reportProviderMetadata(
+ DevicesReportProviderMetadataRequest.builder()
+ .devices(List.of(
+ DeviceMetadata.builder()
+ .deviceId("your-device-id")
+ .ultraloqMetadata(UltraloqMetadata.builder()
+ .timeZone("America/Los_Angeles") // Changed from America/New_York
+ .build())
+ .build()
+ ))
+ .build()
+);
+
+System.out.println("✓ Timezone updated to Pacific Time");
+```
+{% endtab %}
+
+{% tab title="cURL (bash)" %}
+```bash
+curl -X 'POST' \
+ 'https://connect.getseam.com/devices/report_provider_metadata' \
+ -H 'accept: application/json' \
+ -H "Authorization: Bearer ${SEAM_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "devices": [
+ {
+ "device_id": "your-device-id",
+ "ultraloq_metadata": {
+ "time_zone": "America/Los_Angeles"
+ }
+ }
+ ]
+ }'
+```
+{% endtab %}
+{% endtabs %}
+
+**Impact on Existing Access Codes:**
+
+* Existing time-bound access codes maintain their **UTC timestamps**
+* They continue working correctly because Seam stores them in UTC internally
+* Future access codes will use the new timezone for scheduling
+
+***
+
+## Best Practices
+
+### 1. Set Timezone Immediately After Connection
+
+Configure the timezone as soon as you connect an Ultraloq device, before users try to create time-bound access codes:
+
+```python
+# Good practice: Configure timezone right after connection
+devices = seam.devices.list(connected_account_id=account_id)
+
+seam.devices.report_provider_metadata(
+ devices=[
+ {
+ "device_id": device.device_id,
+ "ultraloq_metadata": {"time_zone": user_timezone}
+ }
+ for device in devices
+ ]
+)
+```
+
+### 2. Check Warnings Before Creating Time-Bound Codes
+
+Always check for the `ultraloq_time_zone_unknown` warning before creating time-bound access codes:
+
+```python
+def can_create_time_bound_codes(device):
+ return not any(
+ w.warning_code == "ultraloq_time_zone_unknown"
+ for w in device.warnings
+ )
+
+if can_create_time_bound_codes(device):
+ # Safe to create time-bound codes
+ seam.access_codes.create(
+ device_id=device.device_id,
+ starts_at="...",
+ ends_at="..."
+ )
+else:
+ # Prompt user to configure timezone
+ print("Configure device timezone before creating time-bound codes")
+```
+
+### 3. Use device.location.timezone for Reference
+
+After configuration, you can reference the configured timezone from `device.location.timezone`:
+
+```python
+device = seam.devices.get(device_id="...")
+
+if device.location and device.location.timezone:
+ print(f"Device is in {device.location.timezone}")
+ # Use this timezone for display or calculations
+```
+
+### 4. Provide Clear UI Guidance
+
+In your application UI, guide users to select the correct timezone:
+
+```
+┌─────────────────────────────────────────────┐
+│ ⚠️ Action Required: Set Device Timezone │
+│ │
+│ Your Ultraloq device needs a timezone to │
+│ support scheduled access codes. │
+│ │
+│ Device: Front Door Lock │
+│ │
+│ Select timezone: [America/New_York ▼] │
+│ │
+│ [ Configure Timezone ] │
+└─────────────────────────────────────────────┘
+```
+
+***
+
+## Troubleshooting
+
+### Invalid Timezone Error
+
+**Problem:** You receive an error like `"Invalid timezone. Must be a valid IANA timezone string."`
+
+**Solution:**
+* Verify you're using a valid IANA timezone (e.g., `"America/New_York"`, not `"EST"`)
+* Check for typos in the timezone string
+* Refer to the [IANA timezone list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
+
+### Warning Not Cleared After Configuration
+
+**Problem:** The `ultraloq_time_zone_unknown` warning persists after calling `report_provider_metadata`.
+
+**Solution:**
+* Verify the API call succeeded (check for errors in the response)
+* Refresh the device by calling `seam.devices.get()` again
+* Ensure you used the correct `device_id`
+* Wait a few seconds and check again (state updates are usually immediate but may take a moment)
+
+### Time-Bound Codes Still Failing
+
+**Problem:** Time-bound access codes still fail to create after configuring timezone.
+
+**Solution:**
+* Verify `device.properties.ultraloq_metadata.time_zone` is not `null`
+* Check that `device.warnings` does not contain `ultraloq_time_zone_unknown`
+* Ensure you're providing both `starts_at` and `ends_at` in your access code request
+* Verify your timestamps are valid ISO 8601 UTC strings
+
+***
+
+## API Reference
+
+For complete API documentation, see:
+
+* [POST /devices/report_provider_metadata](../../api/devices/report_provider_metadata.md)
+
+***
+
+## Next Steps
+
+Now that you understand timezone configuration, you can:
+
+* **Create time-bound access codes:** See [Creating Ultraloq Access Codes](creating-ultraloq-access-codes.md)
+* **Review the setup guide:** See [Ultraloq Setup Guide](ultraloq-setup-guide.md)
+* **Learn about access code constraints:** See [Understanding Code Constraints](../../capability-guides/smart-locks/access-codes/creating-access-codes/understanding-code-constraints.md)
+
+***
diff --git a/docs/device-and-system-integration-guides/ultraloq-locks/creating-ultraloq-access-codes.md b/docs/device-and-system-integration-guides/ultraloq-locks/creating-ultraloq-access-codes.md
new file mode 100644
index 000000000..bd293e730
--- /dev/null
+++ b/docs/device-and-system-integration-guides/ultraloq-locks/creating-ultraloq-access-codes.md
@@ -0,0 +1,858 @@
+---
+description: Create and manage access codes for Ultraloq locks
+---
+
+# Creating Ultraloq Access Codes
+
+This guide explains how to create and manage access codes for Ultraloq locks using the Seam API.
+
+***
+
+## Overview
+
+Ultraloq locks support two types of access codes:
+
+* **Permanent access codes** — Codes without start or end times that work indefinitely
+* **Time-bound access codes** — Codes that automatically activate and deactivate at specific times
+
+{% hint style="warning" %}
+**Important:** Time-bound access codes require [timezone configuration](configuring-ultraloq-device-timezones.md). Permanent access codes work without timezone configuration.
+{% endhint %}
+
+***
+
+## Before You Begin
+
+To create access codes for an Ultraloq device:
+
+1. Your Ultraloq device must be [connected to Seam](ultraloq-setup-guide.md)
+2. For **time-bound access codes only**: Device timezone must be [configured](configuring-ultraloq-device-timezones.md)
+3. Device must have `can_program_online_access_codes: true`
+
+***
+
+## Creating Permanent Access Codes
+
+Permanent access codes work indefinitely until you delete them. They **do not require timezone configuration**.
+
+{% tabs %}
+{% tab title="Python" %}
+```python
+from seam import Seam
+
+seam = Seam()
+
+# Create permanent access code
+access_code = seam.access_codes.create(
+ device_id="your-device-id",
+ name="Maintenance Team",
+ code="1234" # Optional: auto-generated if omitted
+)
+
+print(f"Access code created: {access_code.code}")
+print(f"Status: {access_code.status}")
+```
+{% endtab %}
+
+{% tab title="JavaScript" %}
+```javascript
+import { Seam } from "seam";
+
+const seam = new Seam();
+
+// Create permanent access code
+const accessCode = await seam.accessCodes.create({
+ device_id: "your-device-id",
+ name: "Maintenance Team",
+ code: "1234" // Optional: auto-generated if omitted
+});
+
+console.log(`Access code created: ${accessCode.code}`);
+console.log(`Status: ${accessCode.status}`);
+```
+{% endtab %}
+
+{% tab title="Ruby" %}
+```ruby
+require "seam"
+
+seam = Seam.new()
+
+# Create permanent access code
+access_code = seam.access_codes.create(
+ device_id: "your-device-id",
+ name: "Maintenance Team",
+ code: "1234" # Optional: auto-generated if omitted
+)
+
+puts "Access code created: #{access_code.code}"
+puts "Status: #{access_code.status}"
+```
+{% endtab %}
+
+{% tab title="PHP" %}
+```php
+access_codes->create(
+ device_id: "your-device-id",
+ name: "Maintenance Team",
+ code: "1234" // Optional: auto-generated if omitted
+);
+
+echo "Access code created: " . $accessCode->code . "\n";
+echo "Status: " . $accessCode->status . "\n";
+```
+{% endtab %}
+
+{% tab title="C#" %}
+```csharp
+using Seam.Client;
+
+var seam = new SeamClient();
+
+// Create permanent access code
+var accessCode = seam.AccessCodes.Create(
+ deviceId: "your-device-id",
+ name: "Maintenance Team",
+ code: "1234" // Optional: auto-generated if omitted
+);
+
+Console.WriteLine($"Access code created: {accessCode.Code}");
+Console.WriteLine($"Status: {accessCode.Status}");
+```
+{% endtab %}
+
+{% tab title="Java" %}
+```java
+import com.seam.api.Seam;
+import com.seam.api.types.AccessCode;
+
+Seam seam = Seam.builder().build();
+
+// Create permanent access code
+AccessCode accessCode = seam.accessCodes().create(
+ AccessCodesCreateRequest.builder()
+ .deviceId("your-device-id")
+ .name("Maintenance Team")
+ .code("1234") // Optional: auto-generated if omitted
+ .build()
+);
+
+System.out.println("Access code created: " + accessCode.getCode());
+System.out.println("Status: " + accessCode.getStatus());
+```
+{% endtab %}
+
+{% tab title="cURL (bash)" %}
+```bash
+curl -X 'POST' \
+ 'https://connect.getseam.com/access_codes/create' \
+ -H 'accept: application/json' \
+ -H "Authorization: Bearer ${SEAM_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "device_id": "your-device-id",
+ "name": "Maintenance Team",
+ "code": "1234"
+ }'
+```
+{% endtab %}
+{% endtabs %}
+
+***
+
+## Creating Time-Bound Access Codes
+
+Time-bound access codes automatically activate and deactivate at specified times. They **require timezone configuration**.
+
+{% hint style="warning" %}
+**Prerequisites:**
+1. Device timezone must be configured (see [Configuring Ultraloq Device Timezones](configuring-ultraloq-device-timezones.md))
+2. Device must not have the `ultraloq_time_zone_unknown` warning
+{% endhint %}
+
+{% tabs %}
+{% tab title="Python" %}
+```python
+from seam import Seam
+from datetime import datetime, timedelta
+
+seam = Seam()
+
+# Define time range in UTC
+starts_at = datetime.utcnow() + timedelta(days=1)
+ends_at = starts_at + timedelta(days=2)
+
+# Create time-bound access code
+access_code = seam.access_codes.create(
+ device_id="your-device-id",
+ name="Weekend Guest",
+ code="5678", # Optional: auto-generated if omitted
+ starts_at=starts_at.isoformat() + "Z",
+ ends_at=ends_at.isoformat() + "Z"
+)
+
+print(f"Access code created: {access_code.code}")
+print(f"Active from {access_code.starts_at} to {access_code.ends_at}")
+print(f"Status: {access_code.status}")
+```
+{% endtab %}
+
+{% tab title="JavaScript" %}
+```javascript
+import { Seam } from "seam";
+
+const seam = new Seam();
+
+// Define time range in UTC
+const startsAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // Tomorrow
+const endsAt = new Date(startsAt.getTime() + 2 * 24 * 60 * 60 * 1000); // +2 days
+
+// Create time-bound access code
+const accessCode = await seam.accessCodes.create({
+ device_id: "your-device-id",
+ name: "Weekend Guest",
+ code: "5678", // Optional: auto-generated if omitted
+ starts_at: startsAt.toISOString(),
+ ends_at: endsAt.toISOString()
+});
+
+console.log(`Access code created: ${accessCode.code}`);
+console.log(`Active from ${accessCode.starts_at} to ${accessCode.ends_at}`);
+console.log(`Status: ${accessCode.status}`);
+```
+{% endtab %}
+
+{% tab title="Ruby" %}
+```ruby
+require "seam"
+require "time"
+
+seam = Seam.new()
+
+# Define time range in UTC
+starts_at = Time.now.utc + (24 * 60 * 60) # Tomorrow
+ends_at = starts_at + (2 * 24 * 60 * 60) # +2 days
+
+# Create time-bound access code
+access_code = seam.access_codes.create(
+ device_id: "your-device-id",
+ name: "Weekend Guest",
+ code: "5678", # Optional: auto-generated if omitted
+ starts_at: starts_at.iso8601,
+ ends_at: ends_at.iso8601
+)
+
+puts "Access code created: #{access_code.code}"
+puts "Active from #{access_code.starts_at} to #{access_code.ends_at}"
+puts "Status: #{access_code.status}"
+```
+{% endtab %}
+
+{% tab title="PHP" %}
+```php
+add(new DateInterval('P2D'));
+
+// Create time-bound access code
+$accessCode = $seam->access_codes->create(
+ device_id: "your-device-id",
+ name: "Weekend Guest",
+ code: "5678", // Optional: auto-generated if omitted
+ starts_at: $startsAt->format(DateTime::ATOM),
+ ends_at: $endsAt->format(DateTime::ATOM)
+);
+
+echo "Access code created: " . $accessCode->code . "\n";
+echo "Active from " . $accessCode->starts_at . " to " . $accessCode->ends_at . "\n";
+echo "Status: " . $accessCode->status . "\n";
+```
+{% endtab %}
+
+{% tab title="C#" %}
+```csharp
+using Seam.Client;
+using System;
+
+var seam = new SeamClient();
+
+// Define time range in UTC
+var startsAt = DateTime.UtcNow.AddDays(1);
+var endsAt = startsAt.AddDays(2);
+
+// Create time-bound access code
+var accessCode = seam.AccessCodes.Create(
+ deviceId: "your-device-id",
+ name: "Weekend Guest",
+ code: "5678", // Optional: auto-generated if omitted
+ startsAt: startsAt,
+ endsAt: endsAt
+);
+
+Console.WriteLine($"Access code created: {accessCode.Code}");
+Console.WriteLine($"Active from {accessCode.StartsAt} to {accessCode.EndsAt}");
+Console.WriteLine($"Status: {accessCode.Status}");
+```
+{% endtab %}
+
+{% tab title="Java" %}
+```java
+import com.seam.api.Seam;
+import com.seam.api.types.AccessCode;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+Seam seam = Seam.builder().build();
+
+// Define time range in UTC
+Instant startsAt = Instant.now().plus(1, ChronoUnit.DAYS);
+Instant endsAt = startsAt.plus(2, ChronoUnit.DAYS);
+
+// Create time-bound access code
+AccessCode accessCode = seam.accessCodes().create(
+ AccessCodesCreateRequest.builder()
+ .deviceId("your-device-id")
+ .name("Weekend Guest")
+ .code("5678") // Optional: auto-generated if omitted
+ .startsAt(startsAt.toString())
+ .endsAt(endsAt.toString())
+ .build()
+);
+
+System.out.println("Access code created: " + accessCode.getCode());
+System.out.println("Active from " + accessCode.getStartsAt() + " to " + accessCode.getEndsAt());
+System.out.println("Status: " + accessCode.getStatus());
+```
+{% endtab %}
+
+{% tab title="cURL (bash)" %}
+```bash
+# Calculate timestamps (requires date command)
+STARTS_AT=$(date -u -d '+1 day' '+%Y-%m-%dT%H:%M:%SZ')
+ENDS_AT=$(date -u -d '+3 days' '+%Y-%m-%dT%H:%M:%SZ')
+
+curl -X 'POST' \
+ 'https://connect.getseam.com/access_codes/create' \
+ -H 'accept: application/json' \
+ -H "Authorization: Bearer ${SEAM_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -d "{
+ \"device_id\": \"your-device-id\",
+ \"name\": \"Weekend Guest\",
+ \"code\": \"5678\",
+ \"starts_at\": \"${STARTS_AT}\",
+ \"ends_at\": \"${ENDS_AT}\"
+ }"
+```
+{% endtab %}
+{% endtabs %}
+
+### How Time Zone Conversion Works
+
+When you create a time-bound access code:
+
+1. **You provide:** UTC timestamps (`starts_at` and `ends_at`)
+2. **Seam converts:** UTC → Device local time using the configured timezone
+3. **Ultraloq schedules:** Code activation/deactivation in device's local time
+4. **Seam stores:** UTC timestamps for consistent time representation
+
+**Example:**
+
+```
+User creates code with:
+ starts_at: "2024-01-20T19:00:00Z" (7:00 PM UTC)
+ ends_at: "2024-01-22T02:00:00Z" (2:00 AM UTC)
+
+Device timezone: "America/New_York" (UTC-5)
+
+Seam converts to local time:
+ Starts: 2024-01-20 14:00 (2:00 PM EST)
+ Ends: 2024-01-21 21:00 (9:00 PM EST)
+
+Ultraloq device activates code from 2:00 PM to 9:00 PM EST.
+```
+
+***
+
+## Access Code Requirements
+
+### Code Format
+
+Ultraloq access codes must be:
+
+* **Numeric only** — Only digits 0-9
+* **4-8 characters long** — Examples: `"1234"`, `"567890"`, `"12345678"`
+
+{% hint style="info" %}
+**Auto-generated codes:** If you omit the `code` parameter, Seam automatically generates a random 4-8 digit numeric code.
+{% endhint %}
+
+### Valid Examples
+
+```python
+# Valid codes
+"1234" # 4 digits
+"56789" # 5 digits
+"123456" # 6 digits
+"1234567" # 7 digits
+"12345678" # 8 digits
+```
+
+### Invalid Examples
+
+```python
+# Invalid codes
+"123" # Too short (< 4 digits)
+"123456789" # Too long (> 8 digits)
+"abcd" # Non-numeric characters
+"12-34" # Special characters not allowed
+```
+
+***
+
+## Checking Device Readiness
+
+Before creating time-bound access codes, verify that the device's timezone is configured:
+
+{% tabs %}
+{% tab title="Python" %}
+```python
+from seam import Seam
+
+seam = Seam()
+
+def can_create_time_bound_codes(device_id):
+ device = seam.devices.get(device_id=device_id)
+
+ # Check for timezone warning
+ has_timezone_warning = any(
+ w.warning_code == "ultraloq_time_zone_unknown"
+ for w in device.warnings
+ )
+
+ return not has_timezone_warning
+
+# Check before creating
+if can_create_time_bound_codes("your-device-id"):
+ # Safe to create time-bound codes
+ access_code = seam.access_codes.create(
+ device_id="your-device-id",
+ starts_at="...",
+ ends_at="..."
+ )
+else:
+ print("Configure device timezone first")
+ print("See: Configuring Ultraloq Device Timezones")
+```
+{% endtab %}
+
+{% tab title="JavaScript" %}
+```javascript
+import { Seam } from "seam";
+
+const seam = new Seam();
+
+async function canCreateTimeBoundCodes(deviceId) {
+ const device = await seam.devices.get({ device_id: deviceId });
+
+ // Check for timezone warning
+ const hasTimezoneWarning = device.warnings.some(
+ w => w.warning_code === "ultraloq_time_zone_unknown"
+ );
+
+ return !hasTimezoneWarning;
+}
+
+// Check before creating
+if (await canCreateTimeBoundCodes("your-device-id")) {
+ // Safe to create time-bound codes
+ const accessCode = await seam.accessCodes.create({
+ device_id: "your-device-id",
+ starts_at: "...",
+ ends_at: "..."
+ });
+} else {
+ console.log("Configure device timezone first");
+ console.log("See: Configuring Ultraloq Device Timezones");
+}
+```
+{% endtab %}
+
+{% tab title="Ruby" %}
+```ruby
+require "seam"
+
+seam = Seam.new()
+
+def can_create_time_bound_codes?(seam, device_id)
+ device = seam.devices.get(device_id: device_id)
+
+ # Check for timezone warning
+ has_timezone_warning = device.warnings.any? do |w|
+ w.warning_code == "ultraloq_time_zone_unknown"
+ end
+
+ !has_timezone_warning
+end
+
+# Check before creating
+if can_create_time_bound_codes?(seam, "your-device-id")
+ # Safe to create time-bound codes
+ access_code = seam.access_codes.create(
+ device_id: "your-device-id",
+ starts_at: "...",
+ ends_at: "..."
+ )
+else
+ puts "Configure device timezone first"
+ puts "See: Configuring Ultraloq Device Timezones"
+end
+```
+{% endtab %}
+
+{% tab title="PHP" %}
+```php
+devices->get(device_id: $deviceId);
+
+ // Check for timezone warning
+ foreach ($device->warnings as $warning) {
+ if ($warning->warning_code === "ultraloq_time_zone_unknown") {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+// Check before creating
+if (canCreateTimeBoundCodes($seam, "your-device-id")) {
+ // Safe to create time-bound codes
+ $accessCode = $seam->access_codes->create(
+ device_id: "your-device-id",
+ starts_at: "...",
+ ends_at: "..."
+ );
+} else {
+ echo "Configure device timezone first\n";
+ echo "See: Configuring Ultraloq Device Timezones\n";
+}
+```
+{% endtab %}
+{% endtabs %}
+
+***
+
+## Disabled Access Codes
+
+Users can disable access codes through the Ultraloq mobile app. When this happens, Seam detects the change and adds a warning to the access code.
+
+### Detecting Disabled Codes
+
+{% tabs %}
+{% tab title="Python" %}
+```python
+from seam import Seam
+
+seam = Seam()
+
+access_code = seam.access_codes.get(
+ access_code_id="your-access-code-id"
+)
+
+# Check for disabled warning
+is_disabled = any(
+ w.warning_code == "ultraloq_access_code_disabled"
+ for w in access_code.warnings
+)
+
+if is_disabled:
+ print("⚠️ Code is disabled on Ultraloq device")
+ print("User must re-enable it in the Ultraloq mobile app")
+```
+{% endtab %}
+
+{% tab title="JavaScript" %}
+```javascript
+import { Seam } from "seam";
+
+const seam = new Seam();
+
+const accessCode = await seam.accessCodes.get({
+ access_code_id: "your-access-code-id"
+});
+
+// Check for disabled warning
+const isDisabled = accessCode.warnings.some(
+ w => w.warning_code === "ultraloq_access_code_disabled"
+);
+
+if (isDisabled) {
+ console.log("⚠️ Code is disabled on Ultraloq device");
+ console.log("User must re-enable it in the Ultraloq mobile app");
+}
+```
+{% endtab %}
+
+{% tab title="Ruby" %}
+```ruby
+require "seam"
+
+seam = Seam.new()
+
+access_code = seam.access_codes.get(
+ access_code_id: "your-access-code-id"
+)
+
+# Check for disabled warning
+is_disabled = access_code.warnings.any? do |w|
+ w.warning_code == "ultraloq_access_code_disabled"
+end
+
+if is_disabled
+ puts "⚠️ Code is disabled on Ultraloq device"
+ puts "User must re-enable it in the Ultraloq mobile app"
+end
+```
+{% endtab %}
+
+{% tab title="PHP" %}
+```php
+access_codes->get(
+ access_code_id: "your-access-code-id"
+);
+
+// Check for disabled warning
+$isDisabled = false;
+foreach ($accessCode->warnings as $warning) {
+ if ($warning->warning_code === "ultraloq_access_code_disabled") {
+ $isDisabled = true;
+ break;
+ }
+}
+
+if ($isDisabled) {
+ echo "⚠️ Code is disabled on Ultraloq device\n";
+ echo "User must re-enable it in the Ultraloq mobile app\n";
+}
+```
+{% endtab %}
+{% endtabs %}
+
+{% hint style="info" %}
+**Resolution:** The user must re-enable the code in the Ultraloq mobile app. Seam cannot programmatically re-enable disabled codes. Once re-enabled in the app, Seam will automatically detect the change and clear the warning.
+{% endhint %}
+
+***
+
+## Validation and Error Handling
+
+### Time-Bound Code Without Timezone
+
+If you attempt to create a time-bound code without configuring the device's timezone:
+
+```json
+{
+ "error": {
+ "type": "invalid_input",
+ "message": "Time zone required for time-bound access codes on Ultraloq devices"
+ }
+}
+```
+
+**Solution:** Configure the device's timezone first using `/devices/report_provider_metadata`. See [Configuring Ultraloq Device Timezones](configuring-ultraloq-device-timezones.md).
+
+### Invalid Code Format
+
+If you provide a code that doesn't meet the 4-8 digit numeric requirement:
+
+```json
+{
+ "error": {
+ "type": "invalid_input",
+ "message": "Access code must be 4-8 digit numeric PIN for Ultraloq devices"
+ }
+}
+```
+
+**Solution:** Use only numeric digits (0-9) and ensure the code is 4-8 characters long.
+
+### Missing Time Bounds
+
+If you provide only `starts_at` or only `ends_at`:
+
+```json
+{
+ "error": {
+ "type": "invalid_input",
+ "message": "Both starts_at and ends_at must be provided together"
+ }
+}
+```
+
+**Solution:** Either provide both `starts_at` and `ends_at`, or omit both for a permanent code.
+
+### Invalid Time Ordering
+
+If `ends_at` is before `starts_at`:
+
+```json
+{
+ "error": {
+ "type": "invalid_input",
+ "message": "ends_at must be after starts_at"
+ }
+}
+```
+
+**Solution:** Ensure `starts_at` comes before `ends_at`.
+
+***
+
+## Best Practices
+
+### 1. Use Auto-Generated Codes
+
+For better security, let Seam generate random codes instead of using predictable patterns:
+
+```python
+# Good: Auto-generated random code
+access_code = seam.access_codes.create(
+ device_id="your-device-id",
+ name="Guest 123"
+ # code parameter omitted - Seam generates random code
+)
+
+# Less secure: Predictable pattern
+access_code = seam.access_codes.create(
+ device_id="your-device-id",
+ name="Guest 123",
+ code="1234" # Easy to guess
+)
+```
+
+### 2. Check Timezone Before Creating Time-Bound Codes
+
+Always verify timezone configuration before attempting to create time-bound codes:
+
+```python
+device = seam.devices.get(device_id="your-device-id")
+
+if not any(w.warning_code == "ultraloq_time_zone_unknown" for w in device.warnings):
+ # Safe to create time-bound codes
+ seam.access_codes.create(device_id=device.device_id, starts_at="...", ends_at="...")
+else:
+ # Configure timezone first
+ print("Configure timezone before creating time-bound codes")
+```
+
+### 3. Monitor Access Code Warnings
+
+Regularly check access code warnings to detect disabled codes:
+
+```python
+access_codes = seam.access_codes.list(device_id="your-device-id")
+
+disabled_codes = [
+ code for code in access_codes
+ if any(w.warning_code == "ultraloq_access_code_disabled" for w in code.warnings)
+]
+
+if disabled_codes:
+ print(f"⚠️ {len(disabled_codes)} codes are disabled")
+ # Notify user to re-enable in Ultraloq app
+```
+
+### 4. Use UTC Timestamps
+
+Always provide timestamps in UTC (ISO 8601 format with 'Z' suffix):
+
+```python
+# Good: UTC timestamp
+starts_at = "2024-01-20T15:00:00Z"
+
+# Bad: Local time without timezone info
+starts_at = "2024-01-20T10:00:00" # Ambiguous!
+```
+
+***
+
+## Troubleshooting
+
+### Code Not Appearing on Device
+
+If an access code doesn't appear on the physical device:
+
+1. Verify the code status is `set` (not `setting` or `unset`)
+2. Check for warnings on the access code
+3. Ensure the device is online and connected to Wi-Fi
+4. Wait a few minutes for synchronization
+
+### Time-Bound Code Activates at Wrong Time
+
+If a time-bound code activates at an unexpected time:
+
+1. Verify the device's timezone is correctly configured
+2. Check `device.properties.ultraloq_metadata.time_zone`
+3. Ensure you provided UTC timestamps (with 'Z' suffix)
+4. Recalculate the local time conversion to verify correctness
+
+### Code Validation Errors
+
+If you receive validation errors when creating codes:
+
+1. **Invalid format:** Ensure code is 4-8 numeric digits
+2. **Timezone required:** Configure device timezone for time-bound codes
+3. **Missing time bounds:** Provide both `starts_at` and `ends_at`, or neither
+4. **Invalid ordering:** Ensure `starts_at` is before `ends_at`
+
+***
+
+## API Reference
+
+For complete API documentation, see:
+
+* [POST /access_codes/create](../../api/access_codes/create.md)
+* [GET /access_codes/get](../../api/access_codes/get.md)
+* [GET /access_codes/list](../../api/access_codes/list.md)
+* [POST /access_codes/delete](../../api/access_codes/delete.md)
+
+***
+
+## Next Steps
+
+* **Learn about timezone configuration:** See [Configuring Ultraloq Device Timezones](configuring-ultraloq-device-timezones.md)
+* **Understand code constraints:** See [Understanding Code Constraints](../../capability-guides/smart-locks/access-codes/creating-access-codes/understanding-code-constraints.md)
+* **Review the setup guide:** See [Ultraloq Setup Guide](ultraloq-setup-guide.md)
+* **Explore access code webhooks:** See [Webhooks](../../core-concepts/webhooks.md)
+
+***
diff --git a/docs/device-and-system-integration-guides/ultraloq-locks/ultraloq-setup-guide.md b/docs/device-and-system-integration-guides/ultraloq-locks/ultraloq-setup-guide.md
new file mode 100644
index 000000000..31075bd63
--- /dev/null
+++ b/docs/device-and-system-integration-guides/ultraloq-locks/ultraloq-setup-guide.md
@@ -0,0 +1,1470 @@
+---
+description: Step-by-step instructions for connecting Ultraloq devices to Seam
+---
+
+# Ultraloq Setup Guide
+
+This guide walks you through connecting Ultraloq locks to Seam and configuring them for use with the Seam API.
+
+## Before You Begin
+
+To follow this guide, you need:
+
+* A Seam account (create one at [console.seam.co](https://console.seam.co))
+* An API key from your Seam workspace
+* An Ultraloq account with at least one lock configured
+
+{% hint style="info" %}
+If you're testing the integration, you can use a [sandbox workspace](../../core-concepts/workspaces/#sandbox-workspaces) with test Ultraloq devices.
+{% endhint %}
+
+***
+
+## Step 1: Create a Connect Webview
+
+Create a [Connect Webview](../../core-concepts/connect-webviews/) to enable the Ultraloq device owner to authorize Seam to access their Ultraloq account.
+
+{% tabs %}
+{% tab title="Python" %}
+```python
+from seam import Seam
+
+seam = Seam()
+
+webview = seam.connect_webviews.create(
+ accepted_providers=["ultraloq"],
+ custom_redirect_url="https://your-app.com/oauth/callback"
+)
+
+print(webview.url)
+```
+{% endtab %}
+
+{% tab title="JavaScript" %}
+```javascript
+import { Seam } from "seam";
+
+const seam = new Seam();
+
+const webview = await seam.connectWebviews.create({
+ accepted_providers: ["ultraloq"],
+ custom_redirect_url: "https://your-app.com/oauth/callback"
+});
+
+console.log(webview.url);
+```
+{% endtab %}
+
+{% tab title="Ruby" %}
+```ruby
+require "seam"
+
+seam = Seam.new()
+
+webview = seam.connect_webviews.create(
+ accepted_providers: ["ultraloq"],
+ custom_redirect_url: "https://your-app.com/oauth/callback"
+)
+
+puts webview.url
+```
+{% endtab %}
+
+{% tab title="PHP" %}
+```php
+connect_webviews->create(
+ accepted_providers: ["ultraloq"],
+ custom_redirect_url: "https://your-app.com/oauth/callback"
+);
+
+echo $webview->url;
+```
+{% endtab %}
+
+{% tab title="C#" %}
+```csharp
+using Seam.Client;
+
+var seam = new SeamClient();
+
+var webview = seam.ConnectWebviews.Create(
+ acceptedProviders: new[] { "ultraloq" },
+ customRedirectUrl: "https://your-app.com/oauth/callback"
+);
+
+Console.WriteLine(webview.Url);
+```
+{% endtab %}
+
+{% tab title="Java" %}
+```java
+import com.seam.api.Seam;
+import com.seam.api.types.ConnectWebview;
+
+Seam seam = Seam.builder().build();
+
+ConnectWebview webview = seam.connectWebviews().create(
+ ConnectWebviewsCreateRequest.builder()
+ .acceptedProviders(List.of("ultraloq"))
+ .customRedirectUrl("https://your-app.com/oauth/callback")
+ .build()
+);
+
+System.out.println(webview.getUrl());
+```
+{% endtab %}
+
+{% tab title="cURL (bash)" %}
+```bash
+curl -X 'POST' \
+ 'https://connect.getseam.com/connect_webviews/create' \
+ -H 'accept: application/json' \
+ -H "Authorization: Bearer ${SEAM_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "accepted_providers": ["ultraloq"],
+ "custom_redirect_url": "https://your-app.com/oauth/callback"
+ }'
+```
+{% endtab %}
+{% endtabs %}
+
+***
+
+## Step 2: User Authorization
+
+Direct the user to the Connect Webview URL returned in Step 1. The user will be prompted to:
+
+1. Sign in to their Ultraloq account
+2. Authorize Seam to access their Ultraloq devices
+
+After authorization, the user is redirected to your `custom_redirect_url` with the `connect_webview_id` as a query parameter.
+
+***
+
+## Step 3: Verify Connection
+
+Wait for the Connect Webview status to change to `authorized`, indicating that the connection was successful. You can either poll the Connect Webview or use [webhooks](../../core-concepts/webhooks.md) to be notified when the status changes.
+
+{% tabs %}
+{% tab title="Python" %}
+```python
+from seam import Seam
+
+seam = Seam()
+
+# Poll until authorized
+webview = seam.connect_webviews.get(
+ connect_webview_id=webview.connect_webview_id
+)
+
+if webview.status == "authorized":
+ print("Connection successful!")
+ print(f"Connected account ID: {webview.connected_account_id}")
+```
+{% endtab %}
+
+{% tab title="JavaScript" %}
+```javascript
+import { Seam } from "seam";
+
+const seam = new Seam();
+
+// Poll until authorized
+const webview = await seam.connectWebviews.get({
+ connect_webview_id: webview.connect_webview_id
+});
+
+if (webview.status === "authorized") {
+ console.log("Connection successful!");
+ console.log(`Connected account ID: ${webview.connected_account_id}`);
+}
+```
+{% endtab %}
+
+{% tab title="Ruby" %}
+```ruby
+require "seam"
+
+seam = Seam.new()
+
+# Poll until authorized
+webview = seam.connect_webviews.get(
+ connect_webview_id: webview.connect_webview_id
+)
+
+if webview.status == "authorized"
+ puts "Connection successful!"
+ puts "Connected account ID: #{webview.connected_account_id}"
+end
+```
+{% endtab %}
+
+{% tab title="PHP" %}
+```php
+connect_webviews->get(
+ connect_webview_id: $webview->connect_webview_id
+);
+
+if ($webview->status === "authorized") {
+ echo "Connection successful!\n";
+ echo "Connected account ID: " . $webview->connected_account_id;
+}
+```
+{% endtab %}
+
+{% tab title="C#" %}
+```csharp
+using Seam.Client;
+
+var seam = new SeamClient();
+
+// Poll until authorized
+var webview = seam.ConnectWebviews.Get(
+ connectWebviewId: webview.ConnectWebviewId
+);
+
+if (webview.Status == "authorized")
+{
+ Console.WriteLine("Connection successful!");
+ Console.WriteLine($"Connected account ID: {webview.ConnectedAccountId}");
+}
+```
+{% endtab %}
+
+{% tab title="Java" %}
+```java
+import com.seam.api.Seam;
+import com.seam.api.types.ConnectWebview;
+
+Seam seam = Seam.builder().build();
+
+// Poll until authorized
+ConnectWebview webview = seam.connectWebviews().get(
+ ConnectWebviewsGetRequest.builder()
+ .connectWebviewId(webview.getConnectWebviewId())
+ .build()
+);
+
+if (webview.getStatus().equals("authorized")) {
+ System.out.println("Connection successful!");
+ System.out.println("Connected account ID: " + webview.getConnectedAccountId());
+}
+```
+{% endtab %}
+
+{% tab title="cURL (bash)" %}
+```bash
+curl -X 'POST' \
+ 'https://connect.getseam.com/connect_webviews/get' \
+ -H 'accept: application/json' \
+ -H "Authorization: Bearer ${SEAM_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -d "{
+ \"connect_webview_id\": \"${CONNECT_WEBVIEW_ID}\"
+ }"
+```
+{% endtab %}
+{% endtabs %}
+
+***
+
+## Step 4: List Devices
+
+Once the connection is authorized, retrieve the list of Ultraloq devices associated with the connected account.
+
+{% tabs %}
+{% tab title="Python" %}
+```python
+from seam import Seam
+
+seam = Seam()
+
+devices = seam.devices.list(
+ connected_account_id=webview.connected_account_id
+)
+
+for device in devices:
+ print(f"Device: {device.properties['name']}")
+ print(f"Device ID: {device.device_id}")
+ print(f"Warnings: {device.warnings}")
+ print()
+```
+{% endtab %}
+
+{% tab title="JavaScript" %}
+```javascript
+import { Seam } from "seam";
+
+const seam = new Seam();
+
+const devices = await seam.devices.list({
+ connected_account_id: webview.connected_account_id
+});
+
+for (const device of devices) {
+ console.log(`Device: ${device.properties.name}`);
+ console.log(`Device ID: ${device.device_id}`);
+ console.log(`Warnings:`, device.warnings);
+ console.log();
+}
+```
+{% endtab %}
+
+{% tab title="Ruby" %}
+```ruby
+require "seam"
+
+seam = Seam.new()
+
+devices = seam.devices.list(
+ connected_account_id: webview.connected_account_id
+)
+
+devices.each do |device|
+ puts "Device: #{device.properties['name']}"
+ puts "Device ID: #{device.device_id}"
+ puts "Warnings: #{device.warnings}"
+ puts
+end
+```
+{% endtab %}
+
+{% tab title="PHP" %}
+```php
+devices->list(
+ connected_account_id: $webview->connected_account_id
+);
+
+foreach ($devices as $device) {
+ echo "Device: " . $device->properties->name . "\n";
+ echo "Device ID: " . $device->device_id . "\n";
+ echo "Warnings: " . json_encode($device->warnings) . "\n\n";
+}
+```
+{% endtab %}
+
+{% tab title="C#" %}
+```csharp
+using Seam.Client;
+
+var seam = new SeamClient();
+
+var devices = seam.Devices.List(
+ connectedAccountId: webview.ConnectedAccountId
+);
+
+foreach (var device in devices)
+{
+ Console.WriteLine($"Device: {device.Properties.Name}");
+ Console.WriteLine($"Device ID: {device.DeviceId}");
+ Console.WriteLine($"Warnings: {string.Join(", ", device.Warnings)}");
+ Console.WriteLine();
+}
+```
+{% endtab %}
+
+{% tab title="Java" %}
+```java
+import com.seam.api.Seam;
+import com.seam.api.types.Device;
+
+Seam seam = Seam.builder().build();
+
+List devices = seam.devices().list(
+ DevicesListRequest.builder()
+ .connectedAccountId(webview.getConnectedAccountId())
+ .build()
+);
+
+for (Device device : devices) {
+ System.out.println("Device: " + device.getProperties().getName());
+ System.out.println("Device ID: " + device.getDeviceId());
+ System.out.println("Warnings: " + device.getWarnings());
+ System.out.println();
+}
+```
+{% endtab %}
+
+{% tab title="cURL (bash)" %}
+```bash
+curl -X 'POST' \
+ 'https://connect.getseam.com/devices/list' \
+ -H 'accept: application/json' \
+ -H "Authorization: Bearer ${SEAM_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -d "{
+ \"connected_account_id\": \"${CONNECTED_ACCOUNT_ID}\"
+ }"
+```
+{% endtab %}
+{% endtabs %}
+
+**Expected Output:**
+
+When you first list Ultraloq devices, they will have the `ultraloq_time_zone_unknown` warning:
+
+```json
+{
+ "device_id": "11111111-2222-3333-4444-555555555555",
+ "device_type": "ultraloq_lock",
+ "can_program_online_access_codes": true,
+ "can_remotely_lock": true,
+ "can_remotely_unlock": true,
+ "warnings": [
+ {
+ "warning_code": "ultraloq_time_zone_unknown",
+ "message": "Seam does not know the time zone of the Ultraloq device. Set a time zone to enable time-bound access codes."
+ }
+ ],
+ "properties": {
+ "ultraloq_metadata": {
+ "time_zone": null
+ }
+ }
+}
+```
+
+{% hint style="warning" %}
+**Important:** The `ultraloq_time_zone_unknown` warning indicates that you must configure the device's timezone before creating time-bound access codes. Proceed to Step 5 to configure timezones.
+{% endhint %}
+
+***
+
+## Step 5: Configure Device Timezones
+
+This is a **required step** for Ultraloq devices. You must configure each device's timezone before you can create time-bound access codes.
+
+{% tabs %}
+{% tab title="Python" %}
+```python
+from seam import Seam
+
+seam = Seam()
+
+# Configure timezone for one or more devices
+seam.devices.report_provider_metadata(
+ devices=[
+ {
+ "device_id": device.device_id,
+ "ultraloq_metadata": {
+ "time_zone": "America/New_York"
+ }
+ }
+ ]
+)
+
+print("Timezone configured successfully!")
+```
+{% endtab %}
+
+{% tab title="JavaScript" %}
+```javascript
+import { Seam } from "seam";
+
+const seam = new Seam();
+
+// Configure timezone for one or more devices
+await seam.devices.reportProviderMetadata({
+ devices: [
+ {
+ device_id: device.device_id,
+ ultraloq_metadata: {
+ time_zone: "America/New_York"
+ }
+ }
+ ]
+});
+
+console.log("Timezone configured successfully!");
+```
+{% endtab %}
+
+{% tab title="Ruby" %}
+```ruby
+require "seam"
+
+seam = Seam.new()
+
+# Configure timezone for one or more devices
+seam.devices.report_provider_metadata(
+ devices: [
+ {
+ device_id: device.device_id,
+ ultraloq_metadata: {
+ time_zone: "America/New_York"
+ }
+ }
+ ]
+)
+
+puts "Timezone configured successfully!"
+```
+{% endtab %}
+
+{% tab title="PHP" %}
+```php
+devices->report_provider_metadata(
+ devices: [
+ [
+ "device_id" => $device->device_id,
+ "ultraloq_metadata" => [
+ "time_zone" => "America/New_York"
+ ]
+ ]
+ ]
+);
+
+echo "Timezone configured successfully!";
+```
+{% endtab %}
+
+{% tab title="C#" %}
+```csharp
+using Seam.Client;
+
+var seam = new SeamClient();
+
+// Configure timezone for one or more devices
+seam.Devices.ReportProviderMetadata(
+ devices: new[] {
+ new DeviceMetadata {
+ DeviceId = device.DeviceId,
+ UltraloqMetadata = new UltraloqMetadata {
+ TimeZone = "America/New_York"
+ }
+ }
+ }
+);
+
+Console.WriteLine("Timezone configured successfully!");
+```
+{% endtab %}
+
+{% tab title="Java" %}
+```java
+import com.seam.api.Seam;
+
+Seam seam = Seam.builder().build();
+
+// Configure timezone for one or more devices
+seam.devices().reportProviderMetadata(
+ DevicesReportProviderMetadataRequest.builder()
+ .devices(List.of(
+ DeviceMetadata.builder()
+ .deviceId(device.getDeviceId())
+ .ultraloqMetadata(UltraloqMetadata.builder()
+ .timeZone("America/New_York")
+ .build())
+ .build()
+ ))
+ .build()
+);
+
+System.out.println("Timezone configured successfully!");
+```
+{% endtab %}
+
+{% tab title="cURL (bash)" %}
+```bash
+curl -X 'POST' \
+ 'https://connect.getseam.com/devices/report_provider_metadata' \
+ -H 'accept: application/json' \
+ -H "Authorization: Bearer ${SEAM_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -d "{
+ \"devices\": [
+ {
+ \"device_id\": \"${DEVICE_ID}\",
+ \"ultraloq_metadata\": {
+ \"time_zone\": \"America/New_York\"
+ }
+ }
+ ]
+ }"
+```
+{% endtab %}
+{% endtabs %}
+
+{% hint style="success" %}
+**Valid Timezone Values:** Use IANA timezone strings such as `"America/New_York"`, `"Europe/London"`, or `"Asia/Tokyo"`. Do not use timezone abbreviations like `"EST"` or `"PST"`.
+
+For a complete list of valid timezones, see the [IANA Time Zone Database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
+{% endhint %}
+
+For detailed information about timezone configuration, including best practices and troubleshooting, see [Configuring Ultraloq Device Timezones](configuring-ultraloq-device-timezones.md).
+
+***
+
+## Step 6: Verify Configuration
+
+After configuring the timezone, verify that the warning has been cleared and the timezone is set correctly.
+
+{% tabs %}
+{% tab title="Python" %}
+```python
+from seam import Seam
+
+seam = Seam()
+
+device = seam.devices.get(device_id=device.device_id)
+
+# Check that timezone is configured
+assert device.properties["ultraloq_metadata"]["time_zone"] == "America/New_York"
+
+# Check that warning is cleared
+has_warning = any(
+ w.warning_code == "ultraloq_time_zone_unknown"
+ for w in device.warnings
+)
+assert not has_warning, "Timezone warning should be cleared"
+
+print("✓ Device is ready to create time-bound access codes!")
+```
+{% endtab %}
+
+{% tab title="JavaScript" %}
+```javascript
+import { Seam } from "seam";
+
+const seam = new Seam();
+
+const device = await seam.devices.get({
+ device_id: device.device_id
+});
+
+// Check that timezone is configured
+console.assert(
+ device.properties.ultraloq_metadata.time_zone === "America/New_York",
+ "Timezone should be set"
+);
+
+// Check that warning is cleared
+const hasWarning = device.warnings.some(
+ w => w.warning_code === "ultraloq_time_zone_unknown"
+);
+console.assert(!hasWarning, "Timezone warning should be cleared");
+
+console.log("✓ Device is ready to create time-bound access codes!");
+```
+{% endtab %}
+
+{% tab title="Ruby" %}
+```ruby
+require "seam"
+
+seam = Seam.new()
+
+device = seam.devices.get(device_id: device.device_id)
+
+# Check that timezone is configured
+raise "Timezone not set" unless device.properties["ultraloq_metadata"]["time_zone"] == "America/New_York"
+
+# Check that warning is cleared
+has_warning = device.warnings.any? { |w| w.warning_code == "ultraloq_time_zone_unknown" }
+raise "Timezone warning not cleared" if has_warning
+
+puts "✓ Device is ready to create time-bound access codes!"
+```
+{% endtab %}
+
+{% tab title="PHP" %}
+```php
+devices->get(device_id: $device->device_id);
+
+// Check that timezone is configured
+assert($device->properties->ultraloq_metadata->time_zone === "America/New_York");
+
+// Check that warning is cleared
+$hasWarning = false;
+foreach ($device->warnings as $warning) {
+ if ($warning->warning_code === "ultraloq_time_zone_unknown") {
+ $hasWarning = true;
+ break;
+ }
+}
+assert(!$hasWarning, "Timezone warning should be cleared");
+
+echo "✓ Device is ready to create time-bound access codes!";
+```
+{% endtab %}
+
+{% tab title="C#" %}
+```csharp
+using Seam.Client;
+
+var seam = new SeamClient();
+
+var device = seam.Devices.Get(deviceId: device.DeviceId);
+
+// Check that timezone is configured
+Debug.Assert(
+ device.Properties.UltraloqMetadata.TimeZone == "America/New_York",
+ "Timezone should be set"
+);
+
+// Check that warning is cleared
+var hasWarning = device.Warnings.Any(
+ w => w.WarningCode == "ultraloq_time_zone_unknown"
+);
+Debug.Assert(!hasWarning, "Timezone warning should be cleared");
+
+Console.WriteLine("✓ Device is ready to create time-bound access codes!");
+```
+{% endtab %}
+
+{% tab title="Java" %}
+```java
+import com.seam.api.Seam;
+import com.seam.api.types.Device;
+
+Seam seam = Seam.builder().build();
+
+Device device = seam.devices().get(
+ DevicesGetRequest.builder()
+ .deviceId(device.getDeviceId())
+ .build()
+);
+
+// Check that timezone is configured
+assert device.getProperties().getUltraloqMetadata().getTimeZone().equals("America/New_York");
+
+// Check that warning is cleared
+boolean hasWarning = device.getWarnings().stream()
+ .anyMatch(w -> w.getWarningCode().equals("ultraloq_time_zone_unknown"));
+assert !hasWarning : "Timezone warning should be cleared";
+
+System.out.println("✓ Device is ready to create time-bound access codes!");
+```
+{% endtab %}
+
+{% tab title="cURL (bash)" %}
+```bash
+device=$(curl -X 'POST' \
+ 'https://connect.getseam.com/devices/get' \
+ -H 'accept: application/json' \
+ -H "Authorization: Bearer ${SEAM_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -d "{
+ \"device_id\": \"${DEVICE_ID}\"
+ }")
+
+# Check timezone is set
+echo $device | jq '.device.properties.ultraloq_metadata.time_zone'
+
+# Check warnings are cleared
+echo $device | jq '.device.warnings'
+```
+{% endtab %}
+{% endtabs %}
+
+***
+
+## Next Steps
+
+Now that your Ultraloq devices are connected and configured, you can perform common operations:
+
+### Lock and Unlock Devices
+
+{% tabs %}
+{% tab title="Python" %}
+```python
+from seam import Seam
+
+seam = Seam()
+
+# Lock the door
+seam.locks.lock_door(device_id="your-device-id")
+print("Door locked")
+
+# Unlock the door
+seam.locks.unlock_door(device_id="your-device-id")
+print("Door unlocked")
+```
+{% endtab %}
+
+{% tab title="JavaScript" %}
+```javascript
+import { Seam } from "seam";
+
+const seam = new Seam();
+
+// Lock the door
+await seam.locks.lockDoor({ device_id: "your-device-id" });
+console.log("Door locked");
+
+// Unlock the door
+await seam.locks.unlockDoor({ device_id: "your-device-id" });
+console.log("Door unlocked");
+```
+{% endtab %}
+
+{% tab title="Ruby" %}
+```ruby
+require "seam"
+
+seam = Seam.new()
+
+# Lock the door
+seam.locks.lock_door(device_id: "your-device-id")
+puts "Door locked"
+
+# Unlock the door
+seam.locks.unlock_door(device_id: "your-device-id")
+puts "Door unlocked"
+```
+{% endtab %}
+
+{% tab title="PHP" %}
+```php
+locks->lock_door(device_id: "your-device-id");
+echo "Door locked\n";
+
+// Unlock the door
+$seam->locks->unlock_door(device_id: "your-device-id");
+echo "Door unlocked\n";
+```
+{% endtab %}
+
+{% tab title="C#" %}
+```csharp
+using Seam.Client;
+
+var seam = new SeamClient();
+
+// Lock the door
+seam.Locks.LockDoor(deviceId: "your-device-id");
+Console.WriteLine("Door locked");
+
+// Unlock the door
+seam.Locks.UnlockDoor(deviceId: "your-device-id");
+Console.WriteLine("Door unlocked");
+```
+{% endtab %}
+
+{% tab title="Java" %}
+```java
+import com.seam.api.Seam;
+
+Seam seam = Seam.builder().build();
+
+// Lock the door
+seam.locks().lockDoor(LocksLockDoorRequest.builder()
+ .deviceId("your-device-id")
+ .build());
+System.out.println("Door locked");
+
+// Unlock the door
+seam.locks().unlockDoor(LocksUnlockDoorRequest.builder()
+ .deviceId("your-device-id")
+ .build());
+System.out.println("Door unlocked");
+```
+{% endtab %}
+
+{% tab title="cURL (bash)" %}
+```bash
+# Lock the door
+curl -X 'POST' \
+ 'https://connect.getseam.com/locks/lock_door' \
+ -H 'accept: application/json' \
+ -H "Authorization: Bearer ${SEAM_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -d "{\"device_id\": \"your-device-id\"}"
+
+# Unlock the door
+curl -X 'POST' \
+ 'https://connect.getseam.com/locks/unlock_door' \
+ -H 'accept: application/json' \
+ -H "Authorization: Bearer ${SEAM_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -d "{\"device_id\": \"your-device-id\"}"
+```
+{% endtab %}
+{% endtabs %}
+
+### Create Permanent Access Codes
+
+Permanent access codes work indefinitely and do not require timezone configuration:
+
+{% tabs %}
+{% tab title="Python" %}
+```python
+from seam import Seam
+
+seam = Seam()
+
+# Create permanent access code
+access_code = seam.access_codes.create(
+ device_id="your-device-id",
+ name="Maintenance Team",
+ code="1234" # Optional: auto-generated if omitted
+)
+
+print(f"Created permanent code: {access_code.code}")
+```
+{% endtab %}
+
+{% tab title="JavaScript" %}
+```javascript
+import { Seam } from "seam";
+
+const seam = new Seam();
+
+// Create permanent access code
+const accessCode = await seam.accessCodes.create({
+ device_id: "your-device-id",
+ name: "Maintenance Team",
+ code: "1234" // Optional: auto-generated if omitted
+});
+
+console.log(`Created permanent code: ${accessCode.code}`);
+```
+{% endtab %}
+
+{% tab title="Ruby" %}
+```ruby
+require "seam"
+
+seam = Seam.new()
+
+# Create permanent access code
+access_code = seam.access_codes.create(
+ device_id: "your-device-id",
+ name: "Maintenance Team",
+ code: "1234" # Optional: auto-generated if omitted
+)
+
+puts "Created permanent code: #{access_code.code}"
+```
+{% endtab %}
+
+{% tab title="PHP" %}
+```php
+access_codes->create(
+ device_id: "your-device-id",
+ name: "Maintenance Team",
+ code: "1234" // Optional: auto-generated if omitted
+);
+
+echo "Created permanent code: " . $accessCode->code . "\n";
+```
+{% endtab %}
+
+{% tab title="C#" %}
+```csharp
+using Seam.Client;
+
+var seam = new SeamClient();
+
+// Create permanent access code
+var accessCode = seam.AccessCodes.Create(
+ deviceId: "your-device-id",
+ name: "Maintenance Team",
+ code: "1234" // Optional: auto-generated if omitted
+);
+
+Console.WriteLine($"Created permanent code: {accessCode.Code}");
+```
+{% endtab %}
+
+{% tab title="Java" %}
+```java
+import com.seam.api.Seam;
+import com.seam.api.types.AccessCode;
+
+Seam seam = Seam.builder().build();
+
+// Create permanent access code
+AccessCode accessCode = seam.accessCodes().create(
+ AccessCodesCreateRequest.builder()
+ .deviceId("your-device-id")
+ .name("Maintenance Team")
+ .code("1234") // Optional: auto-generated if omitted
+ .build()
+);
+
+System.out.println("Created permanent code: " + accessCode.getCode());
+```
+{% endtab %}
+
+{% tab title="cURL (bash)" %}
+```bash
+curl -X 'POST' \
+ 'https://connect.getseam.com/access_codes/create' \
+ -H 'accept: application/json' \
+ -H "Authorization: Bearer ${SEAM_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "device_id": "your-device-id",
+ "name": "Maintenance Team",
+ "code": "1234"
+ }'
+```
+{% endtab %}
+{% endtabs %}
+
+### Create Time-Bound Access Codes
+
+Time-bound access codes require timezone configuration (completed in Step 5):
+
+{% tabs %}
+{% tab title="Python" %}
+```python
+from seam import Seam
+from datetime import datetime, timedelta
+
+seam = Seam()
+
+# Define time range
+starts_at = datetime.utcnow() + timedelta(days=1)
+ends_at = starts_at + timedelta(days=2)
+
+# Create time-bound access code
+access_code = seam.access_codes.create(
+ device_id="your-device-id",
+ name="Weekend Guest",
+ starts_at=starts_at.isoformat() + "Z",
+ ends_at=ends_at.isoformat() + "Z"
+)
+
+print(f"Created time-bound code: {access_code.code}")
+print(f"Active from {access_code.starts_at} to {access_code.ends_at}")
+```
+{% endtab %}
+
+{% tab title="JavaScript" %}
+```javascript
+import { Seam } from "seam";
+
+const seam = new Seam();
+
+// Define time range
+const startsAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
+const endsAt = new Date(startsAt.getTime() + 2 * 24 * 60 * 60 * 1000);
+
+// Create time-bound access code
+const accessCode = await seam.accessCodes.create({
+ device_id: "your-device-id",
+ name: "Weekend Guest",
+ starts_at: startsAt.toISOString(),
+ ends_at: endsAt.toISOString()
+});
+
+console.log(`Created time-bound code: ${accessCode.code}`);
+console.log(`Active from ${accessCode.starts_at} to ${accessCode.ends_at}`);
+```
+{% endtab %}
+
+{% tab title="Ruby" %}
+```ruby
+require "seam"
+require "time"
+
+seam = Seam.new()
+
+# Define time range
+starts_at = Time.now.utc + (24 * 60 * 60)
+ends_at = starts_at + (2 * 24 * 60 * 60)
+
+# Create time-bound access code
+access_code = seam.access_codes.create(
+ device_id: "your-device-id",
+ name: "Weekend Guest",
+ starts_at: starts_at.iso8601,
+ ends_at: ends_at.iso8601
+)
+
+puts "Created time-bound code: #{access_code.code}"
+puts "Active from #{access_code.starts_at} to #{access_code.ends_at}"
+```
+{% endtab %}
+
+{% tab title="PHP" %}
+```php
+add(new DateInterval('P2D'));
+
+// Create time-bound access code
+$accessCode = $seam->access_codes->create(
+ device_id: "your-device-id",
+ name: "Weekend Guest",
+ starts_at: $startsAt->format(DateTime::ATOM),
+ ends_at: $endsAt->format(DateTime::ATOM)
+);
+
+echo "Created time-bound code: " . $accessCode->code . "\n";
+echo "Active from " . $accessCode->starts_at . " to " . $accessCode->ends_at . "\n";
+```
+{% endtab %}
+
+{% tab title="C#" %}
+```csharp
+using Seam.Client;
+using System;
+
+var seam = new SeamClient();
+
+// Define time range
+var startsAt = DateTime.UtcNow.AddDays(1);
+var endsAt = startsAt.AddDays(2);
+
+// Create time-bound access code
+var accessCode = seam.AccessCodes.Create(
+ deviceId: "your-device-id",
+ name: "Weekend Guest",
+ startsAt: startsAt,
+ endsAt: endsAt
+);
+
+Console.WriteLine($"Created time-bound code: {accessCode.Code}");
+Console.WriteLine($"Active from {accessCode.StartsAt} to {accessCode.EndsAt}");
+```
+{% endtab %}
+
+{% tab title="Java" %}
+```java
+import com.seam.api.Seam;
+import com.seam.api.types.AccessCode;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+Seam seam = Seam.builder().build();
+
+// Define time range
+Instant startsAt = Instant.now().plus(1, ChronoUnit.DAYS);
+Instant endsAt = startsAt.plus(2, ChronoUnit.DAYS);
+
+// Create time-bound access code
+AccessCode accessCode = seam.accessCodes().create(
+ AccessCodesCreateRequest.builder()
+ .deviceId("your-device-id")
+ .name("Weekend Guest")
+ .startsAt(startsAt.toString())
+ .endsAt(endsAt.toString())
+ .build()
+);
+
+System.out.println("Created time-bound code: " + accessCode.getCode());
+System.out.println("Active from " + accessCode.getStartsAt() + " to " + accessCode.getEndsAt());
+```
+{% endtab %}
+
+{% tab title="cURL (bash)" %}
+```bash
+# Calculate timestamps
+STARTS_AT=$(date -u -d '+1 day' '+%Y-%m-%dT%H:%M:%SZ')
+ENDS_AT=$(date -u -d '+3 days' '+%Y-%m-%dT%H:%M:%SZ')
+
+curl -X 'POST' \
+ 'https://connect.getseam.com/access_codes/create' \
+ -H 'accept: application/json' \
+ -H "Authorization: Bearer ${SEAM_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -d "{
+ \"device_id\": \"your-device-id\",
+ \"name\": \"Weekend Guest\",
+ \"starts_at\": \"${STARTS_AT}\",
+ \"ends_at\": \"${ENDS_AT}\"
+ }"
+```
+{% endtab %}
+{% endtabs %}
+
+### Monitor Device Status
+
+{% tabs %}
+{% tab title="Python" %}
+```python
+from seam import Seam
+
+seam = Seam()
+
+device = seam.devices.get(device_id="your-device-id")
+
+# Check lock status
+print(f"Lock status: {device.properties['locked']}")
+
+# Check online status
+print(f"Online: {device.properties['online']}")
+
+# Check battery level (if available)
+if 'battery_level' in device.properties:
+ print(f"Battery level: {device.properties['battery_level']}")
+
+# Check for warnings
+if device.warnings:
+ print(f"Warnings: {[w.warning_code for w in device.warnings]}")
+```
+{% endtab %}
+
+{% tab title="JavaScript" %}
+```javascript
+import { Seam } from "seam";
+
+const seam = new Seam();
+
+const device = await seam.devices.get({ device_id: "your-device-id" });
+
+// Check lock status
+console.log(`Lock status: ${device.properties.locked}`);
+
+// Check online status
+console.log(`Online: ${device.properties.online}`);
+
+// Check battery level (if available)
+if (device.properties.battery_level) {
+ console.log(`Battery level: ${device.properties.battery_level}`);
+}
+
+// Check for warnings
+if (device.warnings.length > 0) {
+ console.log(`Warnings: ${device.warnings.map(w => w.warning_code)}`);
+}
+```
+{% endtab %}
+
+{% tab title="Ruby" %}
+```ruby
+require "seam"
+
+seam = Seam.new()
+
+device = seam.devices.get(device_id: "your-device-id")
+
+# Check lock status
+puts "Lock status: #{device.properties['locked']}"
+
+# Check online status
+puts "Online: #{device.properties['online']}"
+
+# Check battery level (if available)
+if device.properties['battery_level']
+ puts "Battery level: #{device.properties['battery_level']}"
+end
+
+# Check for warnings
+if device.warnings.any?
+ puts "Warnings: #{device.warnings.map(&:warning_code)}"
+end
+```
+{% endtab %}
+
+{% tab title="PHP" %}
+```php
+devices->get(device_id: "your-device-id");
+
+// Check lock status
+echo "Lock status: " . ($device->properties->locked ? "locked" : "unlocked") . "\n";
+
+// Check online status
+echo "Online: " . ($device->properties->online ? "yes" : "no") . "\n";
+
+// Check battery level (if available)
+if (isset($device->properties->battery_level)) {
+ echo "Battery level: " . $device->properties->battery_level . "\n";
+}
+
+// Check for warnings
+if (count($device->warnings) > 0) {
+ $warningCodes = array_map(fn($w) => $w->warning_code, $device->warnings);
+ echo "Warnings: " . implode(", ", $warningCodes) . "\n";
+}
+```
+{% endtab %}
+
+{% tab title="C#" %}
+```csharp
+using Seam.Client;
+using System.Linq;
+
+var seam = new SeamClient();
+
+var device = seam.Devices.Get(deviceId: "your-device-id");
+
+// Check lock status
+Console.WriteLine($"Lock status: {device.Properties.Locked}");
+
+// Check online status
+Console.WriteLine($"Online: {device.Properties.Online}");
+
+// Check battery level (if available)
+if (device.Properties.BatteryLevel.HasValue)
+{
+ Console.WriteLine($"Battery level: {device.Properties.BatteryLevel}");
+}
+
+// Check for warnings
+if (device.Warnings.Any())
+{
+ var warningCodes = string.Join(", ", device.Warnings.Select(w => w.WarningCode));
+ Console.WriteLine($"Warnings: {warningCodes}");
+}
+```
+{% endtab %}
+
+{% tab title="Java" %}
+```java
+import com.seam.api.Seam;
+import com.seam.api.types.Device;
+import java.util.stream.Collectors;
+
+Seam seam = Seam.builder().build();
+
+Device device = seam.devices().get(
+ DevicesGetRequest.builder()
+ .deviceId("your-device-id")
+ .build()
+);
+
+// Check lock status
+System.out.println("Lock status: " + device.getProperties().getLocked());
+
+// Check online status
+System.out.println("Online: " + device.getProperties().getOnline());
+
+// Check battery level (if available)
+if (device.getProperties().getBatteryLevel() != null) {
+ System.out.println("Battery level: " + device.getProperties().getBatteryLevel());
+}
+
+// Check for warnings
+if (!device.getWarnings().isEmpty()) {
+ String warningCodes = device.getWarnings().stream()
+ .map(w -> w.getWarningCode())
+ .collect(Collectors.joining(", "));
+ System.out.println("Warnings: " + warningCodes);
+}
+```
+{% endtab %}
+
+{% tab title="cURL (bash)" %}
+```bash
+device=$(curl -X 'POST' \
+ 'https://connect.getseam.com/devices/get' \
+ -H 'accept: application/json' \
+ -H "Authorization: Bearer ${SEAM_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -d "{\"device_id\": \"your-device-id\"}")
+
+# Check lock status
+echo $device | jq '.device.properties.locked'
+
+# Check online status
+echo $device | jq '.device.properties.online'
+
+# Check battery level
+echo $device | jq '.device.properties.battery_level'
+
+# Check warnings
+echo $device | jq '.device.warnings'
+```
+{% endtab %}
+{% endtabs %}
+
+***
+
+## Related Resources
+
+* **[Creating Ultraloq Access Codes](creating-ultraloq-access-codes.md)** - Detailed guide on permanent and time-bound access codes
+* **[Configuring Ultraloq Device Timezones](configuring-ultraloq-device-timezones.md)** - Complete timezone configuration reference
+* **[Lock and Unlock Operations](../../products/smart-locks/lock-and-unlock.md)** - General smart lock control documentation
+* **[Access Codes Overview](../../products/smart-locks/access-codes/)** - Understanding access code management
+
+***
+
+## Troubleshooting
+
+### Devices not appearing after connection
+
+If devices don't appear after connecting your Ultraloq account:
+
+1. Verify that your locks are connected to Wi-Fi in the Ultraloq mobile app
+2. Ensure your Ultraloq account has access to the locks you're trying to connect
+3. Wait a few minutes for the initial sync to complete
+
+### Warning persists after setting timezone
+
+If the `ultraloq_time_zone_unknown` warning persists after setting the timezone:
+
+1. Verify you used a valid IANA timezone string (e.g., `"America/New_York"`, not `"EST"`)
+2. Check that the API call succeeded without errors
+3. Refresh the device by calling `seam.devices.get()` to get the latest state
+
+### Time-bound access codes fail to create
+
+If you receive an error when creating time-bound access codes:
+
+1. Confirm the device's timezone is configured (check `device.properties.ultraloq_metadata.time_zone`)
+2. Verify the `ultraloq_time_zone_unknown` warning is not present in `device.warnings`
+3. Ensure you're providing both `starts_at` and `ends_at` parameters
+
+***