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 + + + + + + + + + + + + + + + + + +
Card TitleBrief descriptionrelative/path.md
+``` + +#### 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 +
+ + + Description + +
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/codegen/data/paths.yaml b/codegen/data/paths.yaml index 913e352a4..3cf13328f 100644 --- a/codegen/data/paths.yaml +++ b/codegen/data/paths.yaml @@ -101,19 +101,15 @@ /access_grants: title: Access Grants - alpha: "**Early Access Preview.** The Access Grants API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design." /access_methods: title: Access Methods - alpha: "**Early Access Preview.** The access methods API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design." /spaces: title: Spaces - alpha: "**Early Access Preview.** The spaces API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design." /customers: title: Customers - alpha: "**Early Access Preview.** The customers API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design." /devices/unmanaged: title: Unmanaged Devices 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/api/access_grants/README.md b/docs/api/access_grants/README.md index 6328f7687..33e6ad853 100644 --- a/docs/api/access_grants/README.md +++ b/docs/api/access_grants/README.md @@ -1,7 +1,4 @@ # Access Grants -{% hint style="info" %} -**Early Access Preview.** The Access Grants API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} ## The access_grant Object diff --git a/docs/api/access_grants/create.md b/docs/api/access_grants/create.md index 97074fca3..471e77499 100644 --- a/docs/api/access_grants/create.md +++ b/docs/api/access_grants/create.md @@ -1,7 +1,4 @@ # Create an Access Grant -{% hint style="info" %} -**Early Access Preview.** The Access Grants API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/access_grants/delete.md b/docs/api/access_grants/delete.md index cb34e2f85..d7ccc3c36 100644 --- a/docs/api/access_grants/delete.md +++ b/docs/api/access_grants/delete.md @@ -1,7 +1,4 @@ # Delete an Access Grant -{% hint style="info" %} -**Early Access Preview.** The Access Grants API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/access_grants/get.md b/docs/api/access_grants/get.md index 5ac84675d..205457cbd 100644 --- a/docs/api/access_grants/get.md +++ b/docs/api/access_grants/get.md @@ -1,7 +1,4 @@ # Get an Access Grant -{% hint style="info" %} -**Early Access Preview.** The Access Grants API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/access_grants/get_related.md b/docs/api/access_grants/get_related.md index 058d1edca..b1b07513a 100644 --- a/docs/api/access_grants/get_related.md +++ b/docs/api/access_grants/get_related.md @@ -1,7 +1,4 @@ # Get related Access Grant resources -{% hint style="info" %} -**Early Access Preview.** The Access Grants API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/access_grants/list.md b/docs/api/access_grants/list.md index 90acc5f31..9738f528e 100644 --- a/docs/api/access_grants/list.md +++ b/docs/api/access_grants/list.md @@ -1,7 +1,4 @@ # List Access Grants -{% hint style="info" %} -**Early Access Preview.** The Access Grants API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/access_grants/request_access_methods.md b/docs/api/access_grants/request_access_methods.md index 067b0700b..290184ec2 100644 --- a/docs/api/access_grants/request_access_methods.md +++ b/docs/api/access_grants/request_access_methods.md @@ -1,7 +1,4 @@ # Add Requested Access Methods to Access Grant -{% hint style="info" %} -**Early Access Preview.** The Access Grants API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/access_grants/update.md b/docs/api/access_grants/update.md index 8edfd74bb..9a449f1e5 100644 --- a/docs/api/access_grants/update.md +++ b/docs/api/access_grants/update.md @@ -1,7 +1,4 @@ # Update an Access Grant -{% hint style="info" %} -**Early Access Preview.** The Access Grants API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/access_methods/README.md b/docs/api/access_methods/README.md index 29af720a5..9ac867dcf 100644 --- a/docs/api/access_methods/README.md +++ b/docs/api/access_methods/README.md @@ -1,7 +1,4 @@ # Access Methods -{% hint style="info" %} -**Early Access Preview.** The access methods API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} ## The access_method Object diff --git a/docs/api/access_methods/delete.md b/docs/api/access_methods/delete.md index b00ddc774..ff8617d20 100644 --- a/docs/api/access_methods/delete.md +++ b/docs/api/access_methods/delete.md @@ -1,7 +1,4 @@ # Delete an Access Method -{% hint style="info" %} -**Early Access Preview.** The access methods API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/access_methods/encode.md b/docs/api/access_methods/encode.md index 9afa15c20..93e85e2cf 100644 --- a/docs/api/access_methods/encode.md +++ b/docs/api/access_methods/encode.md @@ -1,7 +1,4 @@ # Encode an Access Method -{% hint style="info" %} -**Early Access Preview.** The access methods API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/access_methods/get.md b/docs/api/access_methods/get.md index 25360a53b..2d3770d22 100644 --- a/docs/api/access_methods/get.md +++ b/docs/api/access_methods/get.md @@ -1,7 +1,4 @@ # Get an Access Method -{% hint style="info" %} -**Early Access Preview.** The access methods API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/access_methods/get_related.md b/docs/api/access_methods/get_related.md index 1e0b8041b..382aa1dc5 100644 --- a/docs/api/access_methods/get_related.md +++ b/docs/api/access_methods/get_related.md @@ -1,7 +1,4 @@ # Get related Access Method resources -{% hint style="info" %} -**Early Access Preview.** The access methods API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/access_methods/list.md b/docs/api/access_methods/list.md index 2b8373ec5..e20d3f2d5 100644 --- a/docs/api/access_methods/list.md +++ b/docs/api/access_methods/list.md @@ -1,7 +1,4 @@ # List Access Methods -{% hint style="info" %} -**Early Access Preview.** The access methods API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/customers/README.md b/docs/api/customers/README.md index 3019cf867..662d08903 100644 --- a/docs/api/customers/README.md +++ b/docs/api/customers/README.md @@ -1,7 +1,4 @@ # Customers -{% hint style="info" %} -**Early Access Preview.** The customers API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} ## Endpoints diff --git a/docs/api/customers/create_portal.md b/docs/api/customers/create_portal.md index b85abf998..c96021c1c 100644 --- a/docs/api/customers/create_portal.md +++ b/docs/api/customers/create_portal.md @@ -1,7 +1,4 @@ # Create Customer Portal -{% hint style="info" %} -**Early Access Preview.** The customers API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/customers/delete_data.md b/docs/api/customers/delete_data.md index 59f6648d8..e6663c43e 100644 --- a/docs/api/customers/delete_data.md +++ b/docs/api/customers/delete_data.md @@ -1,7 +1,4 @@ # Delete Customer Data -{% hint style="info" %} -**Early Access Preview.** The customers API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/customers/push_data.md b/docs/api/customers/push_data.md index 953898bd4..f0890ecdb 100644 --- a/docs/api/customers/push_data.md +++ b/docs/api/customers/push_data.md @@ -1,7 +1,4 @@ # Push Customer Data -{% hint style="info" %} -**Early Access Preview.** The customers API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/spaces/README.md b/docs/api/spaces/README.md index 495c57701..1bc649799 100644 --- a/docs/api/spaces/README.md +++ b/docs/api/spaces/README.md @@ -1,7 +1,4 @@ # Spaces -{% hint style="info" %} -**Early Access Preview.** The spaces API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} ## The space Object diff --git a/docs/api/spaces/add_acs_entrances.md b/docs/api/spaces/add_acs_entrances.md index 171968192..2c08599ca 100644 --- a/docs/api/spaces/add_acs_entrances.md +++ b/docs/api/spaces/add_acs_entrances.md @@ -1,7 +1,4 @@ # Add Entrances to a Space -{% hint style="info" %} -**Early Access Preview.** The spaces API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/spaces/add_devices.md b/docs/api/spaces/add_devices.md index dd804f2c8..c7c437500 100644 --- a/docs/api/spaces/add_devices.md +++ b/docs/api/spaces/add_devices.md @@ -1,7 +1,4 @@ # Add Devices to a Space -{% hint style="info" %} -**Early Access Preview.** The spaces API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/spaces/create.md b/docs/api/spaces/create.md index 49afc73e5..740ce74ed 100644 --- a/docs/api/spaces/create.md +++ b/docs/api/spaces/create.md @@ -1,7 +1,4 @@ # Create a Space -{% hint style="info" %} -**Early Access Preview.** The spaces API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/spaces/delete.md b/docs/api/spaces/delete.md index 959839233..24d68e18c 100644 --- a/docs/api/spaces/delete.md +++ b/docs/api/spaces/delete.md @@ -1,7 +1,4 @@ # Delete a Space -{% hint style="info" %} -**Early Access Preview.** The spaces API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/spaces/get.md b/docs/api/spaces/get.md index 2d9ec0061..ad46ebab4 100644 --- a/docs/api/spaces/get.md +++ b/docs/api/spaces/get.md @@ -1,7 +1,4 @@ # Get a Space -{% hint style="info" %} -**Early Access Preview.** The spaces API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/spaces/get_related.md b/docs/api/spaces/get_related.md index ecfb36353..f4bc0a7eb 100644 --- a/docs/api/spaces/get_related.md +++ b/docs/api/spaces/get_related.md @@ -1,7 +1,4 @@ # Get related Space resources -{% hint style="info" %} -**Early Access Preview.** The spaces API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/spaces/list.md b/docs/api/spaces/list.md index b0d540dbd..11c27e8fb 100644 --- a/docs/api/spaces/list.md +++ b/docs/api/spaces/list.md @@ -1,7 +1,4 @@ # List Spaces -{% hint style="info" %} -**Early Access Preview.** The spaces API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/spaces/remove_acs_entrances.md b/docs/api/spaces/remove_acs_entrances.md index 0a07244e4..1ca4a2f57 100644 --- a/docs/api/spaces/remove_acs_entrances.md +++ b/docs/api/spaces/remove_acs_entrances.md @@ -1,7 +1,4 @@ # Remove Entrances from a Space -{% hint style="info" %} -**Early Access Preview.** The spaces API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/spaces/remove_devices.md b/docs/api/spaces/remove_devices.md index b04f90b24..c9d8587b5 100644 --- a/docs/api/spaces/remove_devices.md +++ b/docs/api/spaces/remove_devices.md @@ -1,7 +1,4 @@ # Remove Devices from a Space -{% hint style="info" %} -**Early Access Preview.** The spaces API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/api/spaces/update.md b/docs/api/spaces/update.md index 3672d4e43..bf057e804 100644 --- a/docs/api/spaces/update.md +++ b/docs/api/spaces/update.md @@ -1,7 +1,4 @@ # Update a Space -{% hint style="info" %} -**Early Access Preview.** The spaces API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - [Request Parameters](#request-parameters) - [Response](#response) diff --git a/docs/capability-guides/access-grants/README.md b/docs/capability-guides/access-grants/README.md index f32837fd8..2b9eefa8a 100644 --- a/docs/capability-guides/access-grants/README.md +++ b/docs/capability-guides/access-grants/README.md @@ -6,17 +6,13 @@ description: >- # Access Grants -{% hint style="info" %} -**Early Access Preview.** The Access Grants API is currently in Beta. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - Seam Access Grants provide a unified, intuitive, and streamlined way to grant and manage user access across multiple access system providers. With a single command you can define the "who, where, when, and how" for assigning a user access to entrances and other access points. Access Grants support plastic cards, PIN codes, and mobile keys, including Instant Keys. You can even create multiple access methods using a single command.
Use Access Grants to specify the characteristics of access.

Use Access Grants to specify the characteristics of access.

An Access Grant includes the following characteristics: -
CharacteristicCreation ParameterDescription
Whouser_identity_id or user_identityThe user to whom to grant access. You can either create a user identity separately and specify the ID to the Access Grant or create a new user identity as part of the Access Grant creation action.
Whereacs_entrance_ids or space_idsThe entrances to which to grant access. You can specify one or more entrances by ID. Alternately, you can define spaces (currently in Alpha and available as an early access preview) that contain groups of related entrances and then specify one or more spaces by ID.
Whenstarts_at and ends_atThe access schedule.
Howrequested_access_methods and modeThe access methods that you want to grant for the user. In each requested_access_method, specify the desired mode of access, such as a PIN code, key card, or mobile key (with an Instant Key).
+
CharacteristicCreation ParameterDescription
Whouser_identity_id or user_identityThe user to whom to grant access. You can either create a user identity separately and specify the ID to the Access Grant or create a new user identity as part of the Access Grant creation action.
Whereacs_entrance_ids or space_idsThe entrances to which to grant access. You can specify one or more entrances by ID. Alternately, you can define spaces that contain groups of related entrances and then specify one or more spaces by ID.
Whenstarts_at and ends_atThe access schedule.
Howrequested_access_methods and modeThe access methods that you want to grant for the user. In each requested_access_method, specify the desired mode of access, such as a PIN code, key card, or mobile key (with an Instant Key).
*** @@ -24,7 +20,7 @@ An Access Grant includes the following characteristics: The Access Grant process consists of the following steps: -
StepDescription
  1. Set up your site for Access Grants.
This step includes confirming hardware capabilities and making sure that you have the required licenses. For example, if you plan to use mobile keys or Instant Keys, your lock hardware must support Bluetooth Low Energy (BLE) keys. Also, to use mobile keys with your access system, you may need to purchase licenses or subscriptions to activate certain software features. These requirements vary by access system. For details, see Setting Up Your Site for Instant Keys and the system integration guide for your access system.
  1. Connect your access system to Seam.
To connect your access system to Seam, we recommend embedding a Connect Webview in your application. The Connect Webview flow guides the property manager through each step of the connection process. For on-premises access systems, use Seam Bridge to connect the access system securely to Seam.
For details, see Connect an Access System to Seam.
  1. (Optional) Set up spaces to organize entrances into logical groups.
You can use spaces to create groups of entrances for efficiency. For example, a user staying in Room 101 may need access to both the Room 101 door, the main entrance door, and the nearest elevator. You could create a space that includes these three access points and then use this space when creating an Access Grant.
For details, see spaces.
Note: Spaces are currently in Alpha. We're actively developing this feature and seeking early feedback at support@seam.co. Expect breaking changes as we refine the design.
  1. Create a user identity.
User identities represent your users—the people to whom you want to grant access. You can create a user identity before creating an Access Grant, or you can create a user identity as part of creating the Access Grant.
See Managing Mobile App User Accounts with User Identities.
  1. Create an Access Grant.
When you create an Access Grant, you specify the user identity to whom you want to grant access, the access schedule, the set of entrances or spaces, and one or more access methods that you want to request.
See Creating an Access Grant Using Entrances and Creating an Access Grant Using Spaces.
You can poll for access method status changes or watch for Access Grant and access method lifecycle events that alert you to next steps, such as how and when to deliver each created access method to your user.
  1. Deliver the access method to the user.

Once Seam alerts you that your access methods are ready, deliver them to your user. Delivery steps depend on the mode of access, such as plastic key card, PIN code, or mobile key.

  • If you have created an access grant that includes a card access method, you may need to encode the card using the Seam encoders API.
  • If you have created an Access Grant that includes a mobile key, you can use the Seam mobile SDKs to develop your mobile app that delivers these mobile keys to your users.
  • Each mobile key also includes an Instant Key URL. To share this Instant Key with your user, send it through text or email or embed it in your own app.

See Delivering Access Methods.

+
StepDescription
  1. Set up your site for Access Grants.
This step includes confirming hardware capabilities and making sure that you have the required licenses. For example, if you plan to use mobile keys or Instant Keys, your lock hardware must support Bluetooth Low Energy (BLE) keys. Also, to use mobile keys with your access system, you may need to purchase licenses or subscriptions to activate certain software features. These requirements vary by access system. For details, see Setting Up Your Site for Instant Keys and the system integration guide for your access system.
  1. Connect your access system to Seam.
To connect your access system to Seam, we recommend embedding a Connect Webview in your application. The Connect Webview flow guides the property manager through each step of the connection process. For on-premises access systems, use Seam Bridge to connect the access system securely to Seam.
For details, see Connect an Access System to Seam.
  1. (Optional) Set up spaces to organize entrances into logical groups.
You can use spaces to create groups of entrances for efficiency. For example, a user staying in Room 101 may need access to both the Room 101 door, the main entrance door, and the nearest elevator. You could create a space that includes these three access points and then use this space when creating an Access Grant.
For details, see spaces.
  1. Create a user identity.
User identities represent your users—the people to whom you want to grant access. You can create a user identity before creating an Access Grant, or you can create a user identity as part of creating the Access Grant.
See Managing Mobile App User Accounts with User Identities.
  1. Create an Access Grant.
When you create an Access Grant, you specify the user identity to whom you want to grant access, the access schedule, the set of entrances or spaces, and one or more access methods that you want to request.
See Creating an Access Grant Using Entrances and Creating an Access Grant Using Spaces.
You can poll for access method status changes or watch for Access Grant and access method lifecycle events that alert you to next steps, such as how and when to deliver each created access method to your user.
  1. Deliver the access method to the user.

Once Seam alerts you that your access methods are ready, deliver them to your user. Delivery steps depend on the mode of access, such as plastic key card, PIN code, or mobile key.

  • If you have created an access grant that includes a card access method, you may need to encode the card using the Seam encoders API.
  • If you have created an Access Grant that includes a mobile key, you can use the Seam mobile SDKs to develop your mobile app that delivers these mobile keys to your users.
  • Each mobile key also includes an Instant Key URL. To share this Instant Key with your user, send it through text or email or embed it in your own app.

See Delivering Access Methods.

*** diff --git a/docs/capability-guides/access-grants/access-grant-quick-start.md b/docs/capability-guides/access-grants/access-grant-quick-start.md index a3a86bb96..991ccc777 100644 --- a/docs/capability-guides/access-grants/access-grant-quick-start.md +++ b/docs/capability-guides/access-grants/access-grant-quick-start.md @@ -6,10 +6,6 @@ description: >- # Access Grant Quick Start -{% hint style="info" %} -**Early Access Preview.** The Access Grants API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - In this quick start, create an Access Grant to give a user access to a set of entrances. Access Grants provide a unified, intuitive, and streamlined way to grant and manage user access across multiple access system providers. With a single command you can define the "who, where, when, and how" for assigning a user access to entrances and other access points. In addition, this quick start shows you that, with Access Grants, it's easy to issue users access through multiple types of access methods, such as key cards, PIN codes, mobile keys, and Instant Keys. diff --git a/docs/capability-guides/access-grants/creating-an-access-grant-using-entrances.md b/docs/capability-guides/access-grants/creating-an-access-grant-using-entrances.md index e59a50d03..acb46c5ef 100644 --- a/docs/capability-guides/access-grants/creating-an-access-grant-using-entrances.md +++ b/docs/capability-guides/access-grants/creating-an-access-grant-using-entrances.md @@ -6,10 +6,6 @@ description: >- # Creating an Access Grant Using Entrances -{% hint style="info" %} -**Early Access Preview.** The Access Grants API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - An Access Grant defines the following characteristics: * User identity: The user to whom you want to grant access. diff --git a/docs/capability-guides/access-grants/creating-an-access-grant-using-spaces.md b/docs/capability-guides/access-grants/creating-an-access-grant-using-spaces.md index 74a42bf80..844255aaa 100644 --- a/docs/capability-guides/access-grants/creating-an-access-grant-using-spaces.md +++ b/docs/capability-guides/access-grants/creating-an-access-grant-using-spaces.md @@ -6,10 +6,6 @@ description: >- # Creating an Access Grant Using Spaces -{% hint style="info" %} -**Early Access Preview.** The Access Grants API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - An Access Grant defines the following characteristics: * User identity: The user to whom you want to grant access. diff --git a/docs/capability-guides/access-grants/deleting-an-access-grant.md b/docs/capability-guides/access-grants/deleting-an-access-grant.md index eecae8414..68f04a5e5 100644 --- a/docs/capability-guides/access-grants/deleting-an-access-grant.md +++ b/docs/capability-guides/access-grants/deleting-an-access-grant.md @@ -4,10 +4,6 @@ description: Learn how to delete an Access Grant. # Deleting an Access Grant -{% hint style="info" %} -**Early Access Preview.** The Access Grants API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - To delete an Access Grant: {% tabs %} diff --git a/docs/capability-guides/access-grants/delivering-access-methods.md b/docs/capability-guides/access-grants/delivering-access-methods.md index f47879ad8..cf1117850 100644 --- a/docs/capability-guides/access-grants/delivering-access-methods.md +++ b/docs/capability-guides/access-grants/delivering-access-methods.md @@ -6,10 +6,6 @@ description: >- # Delivering Access Methods -{% hint style="info" %} -**Early Access Preview.** The Access Grants API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - The `access_method.is_issued` property and event let you know when an access method is ready to deliver to your user. Once an access method is ready to deliver, retrieve the access method by ID. Remember, the returned Access Grant includes the IDs of all the requested access methods. Then, deliver the access method to your user. The delivery mechanism varies for different access method modes. {% hint style="info" %} diff --git a/docs/capability-guides/access-grants/retrieving-access-grants-and-access-methods.md b/docs/capability-guides/access-grants/retrieving-access-grants-and-access-methods.md index 5c8b62543..e89126b48 100644 --- a/docs/capability-guides/access-grants/retrieving-access-grants-and-access-methods.md +++ b/docs/capability-guides/access-grants/retrieving-access-grants-and-access-methods.md @@ -4,10 +4,6 @@ description: Learn how to list and get Access Grants and access methods. # Retrieving Access Grants and Access Methods -{% hint style="info" %} -**Early Access Preview.** The Access Grants API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - You can list all Access Grants, and you can also filter the list by one or more of the following properties: * `acs_entrance_id` diff --git a/docs/capability-guides/access-grants/revoking-an-access-method.md b/docs/capability-guides/access-grants/revoking-an-access-method.md index 918d076d4..433e2bbbf 100644 --- a/docs/capability-guides/access-grants/revoking-an-access-method.md +++ b/docs/capability-guides/access-grants/revoking-an-access-method.md @@ -4,10 +4,6 @@ description: Learn how to revoke an access method so that a user can no longer u # Revoking an Access Method -{% hint style="info" %} -**Early Access Preview.** The Access Grants API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - To revoke an access method, delete it. {% tabs %} diff --git a/docs/capability-guides/access-grants/updating-an-access-grant.md b/docs/capability-guides/access-grants/updating-an-access-grant.md index 95f449a89..3688f5994 100644 --- a/docs/capability-guides/access-grants/updating-an-access-grant.md +++ b/docs/capability-guides/access-grants/updating-an-access-grant.md @@ -4,10 +4,6 @@ description: Learn how to update the time window for an Access Grant # Updating an Access Grant -{% hint style="info" %} -**Early Access Preview.** The Access Grants API is currently in Alpha. We're actively developing it and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - You can update the starting and ending times for an Access Grant. Note that you cannot update the access methods, entrances, or spaces associated with an Access Grant. To change these access characteristics, delete the Access Grant and recreate it. diff --git a/docs/capability-guides/access-systems/working-with-card-encoders-and-scanners/creating-and-encoding-card-access-methods.md b/docs/capability-guides/access-systems/working-with-card-encoders-and-scanners/creating-and-encoding-card-access-methods.md index 155ea19c0..946faefcb 100644 --- a/docs/capability-guides/access-systems/working-with-card-encoders-and-scanners/creating-and-encoding-card-access-methods.md +++ b/docs/capability-guides/access-systems/working-with-card-encoders-and-scanners/creating-and-encoding-card-access-methods.md @@ -8,10 +8,6 @@ description: >-
-{% hint style="info" %} -**Early Access Preview.** The Access Grants and access methods APIs are currently in Alpha. We're actively developing them and seeking early feedback at [support@seam.co](mailto:support@seam.co). Expect breaking changes as we refine the design. -{% endhint %} - Some access control systems require encoding a plastic card with the data necessary to enable access. This process involves creating an access grant that requests a card access method with the required access permissions and then using a card encoder to write the access method onto the card. This process consists of the following basic steps: 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 TypeDescription
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_set

If 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_089

PIN 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_0789

PIN 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_length

Name 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.

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 + +
Connect Ultraloq to SeamFollow the setup guide to connect your Ultraloq account and configure device timezones.ultraloq-setup-guide.md
Configure Device TimezonesLearn how to set device timezones to enable time-bound access codes.configuring-ultraloq-device-timezones.md
Create Access CodesLearn how to create permanent and time-bound access codes for Ultraloq locks.creating-ultraloq-access-codes.md
Order LocksPurchase Ultraloq locks from Amazon.https://www.amazon.com/stores/ULTRALOQ
+ +*** 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 + +***