Seam API
Cloud-to-cloud integration with Seam, which fronts a wide roster of smart-lock vendors (August, Yale, Schlage, Salto, SmartThings, etc.) behind a single REST + events API. Use this when the integrator already pays for Seam, or when one workspace controls a mixed fleet that GEM would otherwise need several vendor drivers for.
What's controlled
- Remote
lock/unlock— the SDK auto-polls Seam'saction_attemptsuntil the lock physically confirms, so commands return real success or a descriptive failure, not fire-and-forget. refresh— pull fresh lock state for every zone immediately.clear_tamper— clear the zone'stamperedflag (Seam does not always emit a "tamper resolved" event).- Access-code workflows —
create_access_code,list_access_codes,update_access_code,delete_access_code,generate_code. Supports ongoing codes, time-bound codes (starts_at+ends_at), and one-time codes. pull_events— fetch recent Seam events for the managed locks. Does not advance the internal auto-poll cursor.
Prerequisites
- A Seam workspace at console.seam.co and an API key with permission to read/manage your locks.
- The target locks already enrolled in the manufacturer app and visible in the
Seam dashboard with non-empty
device_idUUIDs. - Outbound HTTPS to
seam.co. No inbound ports.
Setup
- Mint a Seam API key in the Seam console.
- Add a device with driver
seamapiand paste the key intoapi_key. - Create one zone per lock under a lock / security subsystem. Set
zone.addressto the Seamdevice_id(UUID, case-sensitive). - Reload the device. Zone attributes populate on the next poll.
- For access-code workflows, drive
create_access_code/list_access_codesetc. from a macro or the Script Console.
Attributes
Device — required
| Name | Type | Description |
|---|---|---|
api_key | string (secure) | Seam workspace API key. |
Device — optional
| Name | Type | Default | Description |
|---|---|---|---|
status_interval | int (ms) | 5000 | How often to poll Seam for lock state. Floor is 2000 ms. |
poll_events | bool | false | Whether to also poll Seam's events stream for state-change events between snapshots. |
low_battery_threshold | int (%) | 20 | low_battery is set true when battery_level falls below this. Re-evaluated every poll, so threshold edits take effect on the next tick. |
Zone — address
zone.address is the Seam device_id UUID exactly as shown in the Seam
dashboard, e.g. 1a2b3c4d-5678-90ab-cdef-1234567890ab.
Zone — read-only attributes
| Attribute | Type | Description |
|---|---|---|
state | string | locked or unlocked. |
battery_level | int | Battery percentage 0–100 (Seam reports a 0.0–1.0 float; the driver scales it). |
battery_status | string | Manufacturer category: full, good, low, critical. |
low_battery | bool | True when battery_level is below low_battery_threshold. |
online | bool | Lock currently reachable by Seam. |
tampered | bool | Set true by Seam's device.tampered event. Cleared via clear_tamper. |
device_errors | string | Comma-separated Seam error_codes currently active on the lock (e.g. device_offline,hub_disconnected). |
supported_code_lengths | string | Comma-separated digit counts the lock accepts for access codes (e.g. 4,5,6,7,8). Used by create_access_code for pre-validation. |
max_active_codes | int | Max simultaneous access codes the lock supports. |
Commands
| Name | Args | Notes |
|---|---|---|
lock | address | Returns {success, state, action_attempt_id} on confirm, or {error, failure_type} / {error, timeout: true} on a Seam failure / timeout. |
unlock | address | Same shape as lock. |
refresh | — | Re-pull every managed lock and (if enabled) drain the event queue. |
clear_tamper | address | Set tampered back to false on a zone. |
create_access_code | address, name, code, starts_at, ends_at, is_one_time_use, preferred_code_length | Omit code to let Seam auto-generate a compatible value. If code is provided, the driver pre-validates its length against the zone's supported_code_lengths. |
list_access_codes | address | Returns all codes currently provisioned on the lock. |
update_access_code | access_code_id, name, code, starts_at, ends_at | Only pass the fields you want to change. |
delete_access_code | access_code_id | Remove by Seam access-code id. |
generate_code | address | Ask Seam for a code that matches the lock's constraints, without provisioning it. |
pull_events | since_minutes (default 60) | Fetch recent events for the managed locks. Does not advance the auto-poll cursor — safe to call ad-hoc for debugging. Returns {error} if Seam could not be reached. |
How polling and events fit together
The driver maintains its own poll loop (no webhooks). Each tick lists every
lock in the workspace, applies the snapshot to the matching zone, and — if
poll_events is enabled — drains Seam's events stream since the last cursor.
Events are deduped against a 500-entry in-memory ring so overlapping windows
don't double-fire, and are applied oldest-first so the zone ends on the latest
state. If the events fetch fails, the lock snapshot still counts and the cursor
stays where it was, so the missed window is read on the next tick.
On error the driver backs off exponentially up to 60 s (never faster than
status_interval), then resumes the normal cadence once a poll succeeds. When
Seam rejects the API key (401/403) it retries every 5 minutes instead. The
device's connected flag follows the latest poll result. A repeating failure is
logged once, not on every retry.
Every request to Seam has a timeout (30 s; 60 s for lock / unlock, which
wait for the lock to confirm). A request that never answers fails the poll or
the command rather than stalling it.
Subscribed event types
The events poller scopes itself to the event types the driver actually reacts to, to keep Seam quota usage low:
lock.locked, lock.unlocked, lock.access_denied,
device.connected, device.disconnected,
device.connection_became_flaky, device.connection_stabilized,
device.tampered, device.low_battery, device.battery_status_changed,
device.removed, device.deleted,
action_attempt.lock_door.failed, action_attempt.unlock_door.failed,
access_code.set_on_device, access_code.removed_from_device,
access_code.failed_to_set_on_device, access_code.failed_to_remove_from_device.
Known quirks
- Cloud round-trip adds 1–5 s latency on lock/unlock. The SDK auto-polls Seam until the action attempt resolves, so a successful return means the lock truly moved.
- Seam's
battery_levelis a 0.0–1.0 float; GEM scales it to a 0–100 integer. tamperedis set bydevice.tamperedbut Seam does not emit a corresponding "tamper resolved" event for every vendor — clear withclear_tamper.- The driver does not use webhooks; everything is pull-based.
Troubleshooting
| Symptom | Check |
|---|---|
| Zone state never updates | Confirm zone.address matches the Seam device_id exactly (UUID, case-sensitive). Locks with no matching zone are skipped silently, so an address typo produces no warning. |
Commands return invalid zone for address | The zone address and Seam device_id must match. If you changed the address, reload the device. |
| Lock shows offline but works from the Seam app | Run the refresh command. If persistent, inspect the zone's device_errors attribute for Seam's reason. |
create_access_code rejects the code length | Some locks only accept specific lengths (4–8 digits typical). Omit code to let Seam pick one, or read the zone's supported_code_lengths attribute. |
tampered stays true after you cleared the alarm | Run clear_tamper on that zone. |
lock/unlock returns {error, timeout: true} | The Seam SDK waited for confirmation and never got one, or Seam itself did not answer within 60 s. The lock is likely offline or the hub is unreachable. The lock may still move later. |
Device offline, log says seam poll error: Unauthorized | Seam rejected the API key. Replace api_key (or re-enable the workspace) and reload the device. Until then the driver retries every 5 minutes. |
See also
- August Smart Lock — direct August / Yale Home cloud driver, no Seam dependency.
- Schlage Encode / Sense+ — direct Allegion cloud driver.
- Brivo — commercial access-control cloud, similar shape.