Skip to content

Repository files navigation

Seam JavaScript SDK

npm GitHub Actions

JavaScript SDK for the Seam API written in TypeScript.

Description

Seam makes it easy to integrate IoT devices with your applications. This is an official SDK for the Seam API. Please refer to the official Seam Docs to get started.

The SDK is fully tree-shakeable and optimized for use in both client and server applications.

The repository does not contain the SDK code. Instead, it builds on a core set of Seam modules:

  • @seamapi/cli: Command line interface for interacting with the Seam API.
  • @seamapi/http: JavaScript HTTP client for the Seam API written in TypeScript.
  • @seamapi/webhook: Webhook SDK for the Seam API written in TypeScript.

Installation

Add this as a dependency to your project using npm with

$ npm install seam

Usage

Examples

Note

These examples assume SEAM_API_KEY is set in your environment.

List devices

import { Seam } from 'seam'

const seam = new Seam()
const devices = await seam.devices.list()

Unlock a door

import { Seam } from 'seam'

const seam = new Seam()
const lock = await seam.locks.get({ name: 'Front Door' })
await seam.locks.unlockDoor({ device_id: lock.device_id })

Authentication Methods

The SDK supports several authentication mechanisms. Authentication may be configured by passing the corresponding options directly to the Seam constructor, or with the more ergonomic static factory methods.

Note

Publishable Key authentication is not supported by the constructor and must be configured using Seam.fromPublishableKey.

API Key

An API key is scoped to a single workspace and should only be used on the server. Obtain one from the Seam Console.

// Set the `SEAM_API_KEY` environment variable
const seam = new Seam()

// Pass as the first argument to the constructor
const seam = new Seam('your-api-key')

// Pass as an option to the constructor
const seam = new Seam({ apiKey: 'your-api-key' })

// Use the factory method
const seam = Seam.fromApiKey('your-api-key')

Client Session Token

A Client Session Token is scoped to a client session and should only be used on the client.

// Pass as an option to the constructor
const seam = new Seam({ clientSessionToken: 'some-client-session-token' })

// Use the factory method
const seam = Seam.fromClientSessionToken('some-client-session-token')

The client session token may be updated using

const seam = Seam.fromClientSessionToken('some-client-session-token')

await seam.updateClientSessionToken('some-new-client-session-token')

Publishable Key

A Publishable Key is used by the client to acquire Client Session Token for a workspace. Obtain one from the Seam Console.

Use the async factory method to return a client authenticated with a client session token:

const seam = await Seam.fromPublishableKey(
  'your-publishable-key',
  'some-user-identifier-key',
)

This will get an existing client session matching the user identifier key, or create a new empty client session.

Personal Access Token

A Personal Access Token is scoped to a Seam Console user. Obtain one from the Seam Console. A workspace ID must be provided when using this method and all requests will be scoped to that workspace.

// Set the `SEAM_PERSONAL_ACCESS_TOKEN` and `SEAM_WORKSPACE_ID` environment variables
const seam = new Seam()

// Pass as an option to the constructor
const seam = new Seam({
  personalAccessToken: 'your-personal-access-token',
  workspaceId: 'your-workspace-id',
})

// Use the factory method
const seam = Seam.fromPersonalAccessToken(
  'some-console-session-token',
  'your-workspace-id',
)

Console Session Token

A Console Session Token is used by the Seam Console. This authentication method is only used by internal Seam applications. A workspace ID must be provided when using this method and all requests will be scoped to that workspace.

// Pass as an option to the constructor
const seam = new Seam({
  consoleSessionToken: 'some-console-session-token',
  workspaceId: 'your-workspace-id',
})

// Use the factory method
const seam = Seam.fromConsoleSessionToken(
  'some-console-session-token',
  'your-workspace-id',
)

Action Attempts

Some asynchronous operations, e.g., unlocking a door, return an action attempt. Seam tracks the progress of the requested operation and updates the action attempt when it succeeds or fails.

To make working with action attempts more convenient for applications, this library provides the waitForActionAttempt option and enables it by default.

When the waitForActionAttempt option is enabled, the SDK:

  • Polls the action attempt up to the timeout at the pollingInterval (both in milliseconds).
  • Resolves with a fresh copy of the successful action attempt.
  • Rejects with a SeamActionAttemptFailedError if the action attempt is unsuccessful.
  • Rejects with a SeamActionAttemptTimeoutError if the action attempt is still pending when the timeout is reached.
  • Both errors expose an actionAttempt property.

If you already have an action attempt ID and want to wait for it to resolve, simply use

await seam.actionAttempts.get({ action_attempt_id })

Or, to get the current state of an action attempt by ID without waiting:

await seam.actionAttempts.get(
  { action_attempt_id },
  {
    waitForActionAttempt: false,
  },
)

To disable this behavior, set the default option for the client:

const seam = new Seam({
  apiKey: 'your-api-key',
  waitForActionAttempt: false,
})

await seam.locks.unlockDoor({ device_id })

or the behavior may be configured per-request:

await seam.locks.unlockDoor(
  { device_id },
  {
    waitForActionAttempt: false,
  },
)

The pollingInterval and timeout may be configured for the client or per-request. For example:

import {
  Seam,
  isSeamActionAttemptFailedError,
  isSeamActionAttemptTimeoutError,
} from 'seam'

const seam = new Seam('your-api-key', {
  waitForActionAttempt: {
    pollingInterval: 1000,
    timeout: 5000,
  },
})

const [lock] = await seam.locks.list()

if (lock == null) throw new Error('No locks in this workspace')

try {
  await seam.locks.unlockDoor({ device_id: lock.device_id })
  console.log('Door unlocked')
} catch (err: unknown) {
  if (isSeamActionAttemptFailedError(err)) {
    console.log('Could not unlock the door')
    return
  }

  if (isSeamActionAttemptTimeoutError(err)) {
    console.log('Door took too long to unlock')
    return
  }

  throw err
}

Pagination

Some Seam API endpoints that return lists of resources support pagination. Use the SeamPaginator class to fetch and process resources across multiple pages.

Manually fetch pages with the nextPageCursor

const pages = seam.createPaginator(
  seam.devices.list({
    limit: 20,
  }),
)

const [devices, { hasNextPage, nextPageCursor }] = await pages.firstPage()

if (hasNextPage) {
  const [moreDevices] = await pages.nextPage(nextPageCursor)
}

Resume pagination

Get the first page on initial load:

const params = { limit: 20 }

const pages = seam.createPaginator(seam.devices.list(params))

const [devices, pagination] = await pages.firstPage()

localStorage.setItem('/seam/devices/list', JSON.stringify([params, pagination]))

Get the next page at a later time:

const [params = {}, { hasNextPage = false, nextPageCursor = null } = {}] =
  JSON.parse(localStorage.getItem('/seam/devices/list') ?? '[]')

if (hasNextPage) {
  const pages = seam.createPaginator(seam.devices.list(params))
  const [moreDevices] = await pages.nextPage(nextPageCursor)
}

Iterate over all pages

const pages = seam.createPaginator(
  seam.devices.list({
    limit: 20,
  }),
)

for await (const devices of pages) {
  console.log(`There are ${devices.length} devices on this page.`)
}

Iterate over all resources

const pages = seam.createPaginator(
  seam.devices.list({
    limit: 20,
  }),
)

for await (const device of pages.flatten()) {
  console.log(devices.name)
}

Return all resources across all pages as an array

const pages = seam.createPaginator(
  seam.devices.list({
    limit: 20,
  }),
)

const devices = await pages.flattenToArray()

Requests without a Workspace in scope

Some Seam API endpoints do not require a workspace in scope. The SeamWithoutWorkspace client is not bound to a specific workspace and may use those endpoints with an appropriate authentication method.

Personal Access Token

A Personal Access Token is scoped to a Seam Console user. Obtain one from the Seam Console.

// Set the `SEAM_PERSONAL_ACCESS_TOKEN` environment variable
const seam = new SeamWithoutWorkspace()

// Pass as an option to the constructor
const seam = new SeamWithoutWorkspace({
  personalAccessToken: 'your-personal-access-token',
})

// Use the factory method
const seam = SeamWithoutWorkspace.fromPersonalAccessToken(
  'some-console-session-token',
)

// List workspaces authorized for this Personal Access Token
const workspaces = await seam.workspaces.list()

Console Session Token

A Console Session Token is used by the Seam Console. This authentication method is only used by internal Seam applications.

// Pass as an option to the constructor
const seam = new SeamWithoutWorkspace({
  consoleSessionToken: 'some-console-session-token',
})

// Use the factory method
const seam = SeamWithoutWorkspace.fromConsoleSessionToken(
  'some-console-session-token',
)

// List workspaces authorized for this Seam Console user
const workspaces = await seam.workspaces.list()

Advanced Usage

Additional Options

In addition to the various authentication options, the constructor takes some advanced options that affect behavior.

const seam = new Seam({
  apiKey: 'your-api-key',
  endpoint: 'https://example.com',
  timeout: 30000,
  axiosOptions: {},
  axiosRetryOptions: {},
})

When using the static factory methods, these options may be passed in as the last argument.

const seam = Seam.fromApiKey('some-api-key', {
  endpoint: 'https://example.com',
  timeout: 30000,
  axiosOptions: {},
  axiosRetryOptions: {},
})

Setting the endpoint

Some contexts may need to override the API endpoint, e.g., testing or proxy setups. This option corresponds to the Axios baseURL setting.

Either pass the endpoint option, or set the SEAM_ENDPOINT environment variable.

Setting the request timeout

Requests time out after 30 seconds by default. Pass the timeout option, in milliseconds, to override this:

const seam = new Seam({
  apiKey: 'your-api-key',
  timeout: 60000,
})

Set timeout to 0 to disable the timeout entirely. A request that times out rejects with an Axios ETIMEDOUT error, and is retried according to the retry options.

Configuring the Axios Client

The Axios client and retry behavior may be configured with custom initiation options via axiosOptions and axiosRetryOptions. Options are deep merged with the default options.

Using the Axios Client

The Axios client is exposed and may be used or configured directly:

import { Seam, DevicesListResponse } from 'seam'

const seam = new Seam()

seam.client.interceptors.response.use((response) => {
  console.log(response)
  return response
})

const devices = await seam.client.get<DevicesListResponse>('/devices/list')

Overriding the Client

An Axios compatible client may be provided to create a Seam instance. This API is used internally and is not directly supported.

Alternative endpoint path interface

The SeamEndpoints class offers an alternative path-based interface to every API endpoint. Each endpoint is exposed as simple property that returns the corresponding method from Seam.

import { SeamEndpoints } from 'seam'

const seam = new SeamEndpoints()
const devices = await seam['/devices/list']()

Inspecting the Request

All client methods return an instance of SeamHttpRequest. Inspect the request before it is sent to the server by intentionally not awaiting the SeamHttpRequest:

const seam = new Seam('your-api-key')

const request = seam.devices.list()

console.log(`${request.method} ${request.url}`, JSON.stringify(request.body))

const devices = await request.execute()

Command Line Interface

Every seam command makes its request as soon as every required property is given. When something is missing, the CLI prompts you for it with helpful suggestions.

Pass --interactive (or -i) to always be prompted to review and edit properties before the request is made. The prompt is prefilled with whatever you passed as arguments, so this is the way to add optional properties, or to check a request before making it.

For scripts and CI, pass --non-interactive (or -y) to never be prompted. The command must then be complete: if the command itself is ambiguous, or any required property is missing, the CLI exits with an error naming what is missing instead of asking for it.

To take a project from zero to a working Seam integration, run the [Seam Wizard] from the project's root:

seam wizard

For API commands:

# Login to Seam
seam login

# Select your workspace
seam select workspace

# Interactively select commands to execute
seam

# Create a connect webview to connect devices
seam connect-webviews create

# List devices in your workspace
seam devices list

# Review and edit filters before listing devices
seam devices list --interactive

# List devices, failing instead of prompting
seam devices list --non-interactive

# Fails with: Missing required parameter for /locks/unlock_door: --device-id
seam locks unlock-door --non-interactive

# Fails with: Unknown parameter for /devices/list: --limitt
seam devices list --limitt 5

MY_DOOR=$(seam devices get --name "Front Door" | jq -r '.device.device_id')

# Unlock a lock
seam locks unlock-door --device-id $MY_DOOR

# Create an access code
seam access-codes create --code "1234" --name "My Code"

# List your access codes
seam access-codes list --device-id $MY_DOOR

Output

Only the response is written to stdout, so any command may be piped or redirected. Prompts, progress, and other information are written to stderr.

The response is trimmed to the response key and pagination: no other top level fields are reported.

# The response, and nothing else, ends up in the file
seam devices list > devices.json

# Prompts and progress still show up in the terminal
seam devices list | jq '.devices[].device_id'

Pagination

Every command that paginates accepts --page-cursor to select a page of results, alongside --limit for the size of that page. Each response reports its pagination, whose next_page_cursor is the cursor for the page after it.

# The first page, and the cursor for the next one
seam devices list --limit 2 | jq '.pagination.next_page_cursor'

# The page after it
seam devices list --limit 2 --page-cursor "$CURSOR"

A cursor is opaque: pass it back exactly as it was reported, and do not build one yourself. Run seam <command> --help to see whether a command paginates.

JSON

Request params may be piped or redirected in as a JSON object. Params given as arguments win over params read from stdin.

An argument the command does not accept is an error, so a typo is reported rather than sent. Params read from stdin are passed through as given, so anything the API itself accepts may be sent that way.

# Read params from a file
seam locks unlock-door < params.json

# Or from another program
echo '{"device_id": "'"$MY_DOOR"'"}' | seam locks unlock-door

# --device-id wins over any device_id in params.json
seam devices list --limit 5 < params.json

Pass --json to write the response as JSON. It is enabled automatically whenever stdout is not a terminal, so piping and redirecting produce JSON without passing anything. Pass --no-json to opt out and get the pretty format instead.

# Both write JSON
seam devices list --json
seam devices list | jq

# Pretty printed, even though it is piped
seam devices list --no-json | less

Without a terminal to prompt on, the CLI behaves as though --non-interactive was given: rather than waiting for an answer nobody can give, it exits with an error naming what is missing.

$ echo '{}' | seam locks unlock-door
Missing required parameter for /locks/unlock_door: --device-id

An error exits non-zero. A request that fails reports its error on stdout, so it can be inspected from a pipe; anything else is written to stderr only.

Selecting an endpoint and a workspace

Two settings say where commands go, and one command each stores them:

# Every later command runs against this endpoint
seam select endpoint https://connect.getseam.com

# ...and this workspace
seam select workspace $MY_WORKSPACE

Run either without a value to pick one interactively.

To send a single command somewhere else, pass --endpoint or --workspace-id to that command. They override what is selected for that one invocation and store nothing:

# List devices in another workspace, without switching to it
seam devices list --workspace-id $OTHER_WORKSPACE

# Run one command against a local Seam Connect instance
seam devices list --endpoint http://localhost:3020

# Log in to another endpoint: the token is stored for that endpoint,
# and the selected one is left alone
seam login --endpoint http://localhost:3020 --token $LOCAL_KEY

Because the two flags never store anything, they are refused on the commands that do: seam select endpoint --endpoint <url> is an error, and the value belongs after the command instead.

Environment variables

Everything seam login, seam select workspace, and seam select endpoint store may be given in the environment instead:

  • SEAM_CLI_TOKEN: a Personal Access Token or API Key,
  • SEAM_CLI_WORKSPACE_ID: the workspace requests are made against,
  • SEAM_CLI_ENDPOINT: the Seam API endpoint requests are made to.

Any of them, all of them, or none of them may be set. Each one wins over the corresponding stored value and is in turn overridden by --endpoint or --workspace-id, which makes them useful for CI or for working against another workspace for a whole shell.

# One command against another workspace
SEAM_CLI_WORKSPACE_ID=$OTHER_WORKSPACE seam devices list

# No login needed: authenticate from the environment
export SEAM_CLI_TOKEN=$SEAM_API_KEY
seam devices list

# Work against a local Seam Connect instance
SEAM_CLI_ENDPOINT=http://localhost:3020 seam devices list

An API Key is scoped to a single workspace, so it needs no workspace id. A Personal Access Token works across workspaces, so it needs one from --workspace-id, SEAM_CLI_WORKSPACE_ID, or seam select workspace.

The command that would store an overridden value fails rather than storing something the environment ignores: seam login and seam logout while SEAM_CLI_TOKEN is set, seam select workspace while SEAM_CLI_WORKSPACE_ID is set, and seam select endpoint while SEAM_CLI_ENDPOINT is set. Unset the variable to use those commands.

$ SEAM_CLI_TOKEN=$SEAM_API_KEY seam login
Cannot log in while SEAM_CLI_TOKEN is set: it overrides what would be stored. Unset SEAM_CLI_TOKEN to log in.

Receiving Webhooks

The Seam API implements webhooks using Svix. This SDK exports a thin wrapper SeamWebhook around the svix package. Use it to parse and validate Seam webhook events.

Refer to the Svix docs on Consuming Webhooks for an in-depth guide on best-practices for handling webhooks in your application.

import { env } from 'node:process'

import { SeamWebhook } from 'seam'
import express from 'express'
import bodyParser from 'body-parser'

const app = express()

const webhook = new SeamWebhook(env.SEAM_WEBHOOK_SECRET)

app.post(
  '/webhook',
  bodyParser.raw({ type: 'application/json' }),
  (req, res) => {
    let data
    try {
      data = webhook.verify(req.body, req.headers)
    } catch {
      return res.status(400).send()
    }
    storeEvent(data, (err) => {
      if (err != null) {
        return res.status(500).send()
      }
      res.status(204).send()
    })
  },
)

const storeEvent = (data, callback) => {
  console.log(data)
  callback()
}

app.listen(8080, () => {
  console.log('Ready to receive webhooks at http://localhost:8080/webhook')
})

Development and Testing

Quickstart

$ git clone https://github.com/seamapi/javascript.git
$ cd javascript
$ nvm install
$ npm install

Primary development tasks are defined under scripts in package.json and available via npm run. View them with

$ npm run

Source code

The source code is hosted on GitHub. Clone the project with

$ git clone git@github.com:seamapi/javascript.git

Requirements

You will need Node.js with npm and a Node.js debugging client.

Be sure that all commands run under the correct Node version, e.g., if using nvm, install the correct version with

$ nvm install

Set the active version for each shell session with

$ nvm use

Install the development dependencies with

$ npm install

Publishing

Automatic

New versions are released automatically with semantic-release as long as commits follow the Angular Commit Message Conventions.

Manual

Publish a new version by triggering a version workflow_dispatch on GitHub Actions. The version input will be passed as the first argument to npm-version.

This may be done on the web or using the GitHub CLI with

$ gh workflow run version.yml --raw-field version=<version>

GitHub Actions

GitHub Actions should already be configured: this section is for reference only.

The following repository secrets must be set on GitHub Actions:

  • GH_TOKEN: A personal access token for the bot user with and contents:write permission.
  • GIT_USER_NAME: The GitHub bot user's real name.
  • GIT_USER_EMAIL: The GitHub bot user's email.
  • GPG_PRIVATE_KEY: The GitHub bot user's GPG private key.
  • GPG_PASSPHRASE: The GitHub bot user's GPG passphrase.

Contributing

If using squash merge, edit and ensure the commit message follows the Angular Commit Message Conventions specification. Otherwise, each individual commit must follow the Angular Commit Message Conventions specification.

  1. Create your feature branch (git checkout -b my-new-feature).
  2. Make changes.
  3. Commit your changes (git commit -am 'Add some feature').
  4. Push to the branch (git push origin my-new-feature).
  5. Create a new draft pull request.
  6. Ensure all checks pass.
  7. Mark your pull request ready for review.
  8. Wait for the required approval from the code owners.
  9. Merge when ready.

License

This npm package is licensed under the MIT license.

Warranty

This software is provided by the copyright holders and contributors "as is" and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright holder or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.

About

JavaScript SDK for the Seam API written in TypeScript.

Resources

Stars

17 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages