Skip to content

Node.js SDK

@raclip/sdk is the official Node.js SDK. It's a thin, typed wrapper around the HTTP API — every SDK call corresponds to a documented endpoint.

Install

npm install @raclip/sdk
# or
yarn add @raclip/sdk
# or
pnpm add @raclip/sdk

Requires Node.js 18+. No runtime dependencies — uses the built-in fetch API.

Authenticate

Pass an API key explicitly, or set RACLIP_API_KEY in your environment:

export RACLIP_API_KEY="raclip_prod_…"
import { Client } from '@raclip/sdk'

// Reads RACLIP_API_KEY from the environment
const client = new Client()

// Or pass explicitly
const client = new Client({ apiKey: 'raclip_prod_…' })

The SDK never sends your org_id on the wire — the gateway derives it from the key.

Quickstart

import { Client } from '@raclip/sdk'

const client = new Client()

const devices = await client.devices.list()
console.log(`${devices.total} devices in this org`)

for (const device of devices.devices) {
  console.log(device.device_id, device.status, device.last_seen)
}

if (devices.devices.length > 0) {
  const deviceId = devices.devices[0].device_id
  const calls = await client.calls.list(deviceId, { per_page: 5 })
  for (const call of calls.calls) {
    console.log(call.call_id, call.duration_seconds, 'sec')
    console.log('  download:', call.download_url)
  }
}

The client supports Symbol.asyncDispose — use await using in environments with TypeScript 5.2+:

await using client = new Client()
// client.close() called automatically on scope exit

Download a recording

download_url on a call response is a short-lived URL. You can fetch it with anything that speaks HTTP:

const response = await fetch(call.download_url!)
const buffer = await response.arrayBuffer()

Or use the SDK's streaming helper, which avoids leaking your Authorization header on the redirect:

await client.calls.download(deviceId, call.call_id, 'recording.mp3')

dest accepts a file path (string) or any Node.js Writable stream.

Error handling

Every non-2xx response converts to a typed error so you can catch the cases you care about:

import {
  Client,
  AuthenticationError,
  NotFoundError,
  RateLimitError,
} from '@raclip/sdk'

const client = new Client()

try {
  const call = await client.calls.get('device-id', 'call-id')
} catch (err) {
  if (err instanceof NotFoundError) {
    console.log('call is gone')
  } else if (err instanceof RateLimitError) {
    console.log(`slow down; retry after ${err.retryAfter}s`)
  } else if (err instanceof AuthenticationError) {
    console.log('check your API key')
  }
}

Hierarchy: RaclipError → AuthenticationError, PermissionError, NotFoundError, ValidationError, RateLimitError, APIError, APIConnectionError.


Client

Client is the entry point. All methods are async. Auto-reads RACLIP_API_KEY from the environment if apiKey is omitted.

Constructor

new Client(options?: ClientOptions)
Option Type Default Description
apiKey string process.env.RACLIP_API_KEY API key. Throws AuthenticationError if absent.
baseUrl string production URL Override the API base URL.
timeout number 30 Request timeout in seconds.

Properties

Property Type Description
baseUrl string Resolved base URL (read-only).
devices DevicesResource Device operations.
calls CallsResource Call operations.

Methods

close()

client.close(): void

No-op for the fetch-based transport. Exists for API parity. Use await using or call explicitly for clarity.


Resources

client.devices and client.calls expose all API operations. Every method returns a Promise.

Devices

devices.list(params?)

client.devices.list(params?: DevicesListParams): Promise<DeviceListResponse>

List all devices in the org.

Param Type Default
status string \| null null
page number 1
per_page number 50

devices.get(deviceId)

client.devices.get(deviceId: string): Promise<Device>

Fetch a single device by ID.

devices.status(deviceId)

client.devices.status(deviceId: string): Promise<DeviceStatusSnapshot>

Lightweight online/offline check for a device.

devices.stats()

client.devices.stats(): Promise<DeviceStats>

Org-wide device statistics.


Calls

calls.list(deviceId, params?)

client.calls.list(deviceId: string, params?: CallsListParams): Promise<CallListResponse>

List calls for a device.

Param Type Default
page number 1
per_page number 50
days_back number 30
include_urls boolean true

calls.get(deviceId, callId, params?)

client.calls.get(deviceId: string, callId: string, params?: CallsGetParams): Promise<Call>

Fetch a single call. Pass { download: true } to include download_url.

calls.latest(deviceId, params?)

client.calls.latest(deviceId: string, params?: CallsLatestParams): Promise<Call>

Fetch the most recent call for a device.

calls.statistics(deviceId, params?)

client.calls.statistics(deviceId: string, params?: CallsStatisticsParams): Promise<CallStatistics>

Aggregate statistics for a device's calls. days_back defaults to 30.

calls.info(deviceId, callId)

client.calls.info(deviceId: string, callId: string): Promise<Record<string, unknown>>

Raw call metadata object.

calls.download(deviceId, callId, dest)

client.calls.download(deviceId: string, callId: string, dest: DownloadDest): Promise<DownloadDest>

Stream the recording to dest. dest is a file path string or a Node.js Writable. Returns dest.


Models

Every API response is parsed into a typed TypeScript interface.

Device

interface Device {
  device_id: string
  org_id: string | null
  hardware_id: string
  mac_address: string
  serial_number: string | null
  firmware_version: string
  device_model: string
  unit_id: string | null
  location_id: string | null
  employee_id: string | null
  custom_fields: Record<string, unknown> | null
  status: string
  last_seen: Date | null
  last_registration: Date
  assigned_at: Date | null
  created_at: Date
}

DeviceListResponse

interface DeviceListResponse {
  devices: Device[]
  total: number
  page: number
  per_page: number
}

DeviceStats

interface DeviceStats {
  org_id: string
  total_devices: number
  assigned_devices: number
  unassigned_devices: number
  online_devices: number
  offline_devices: number
  recent_registrations_7d: number
  device_status_breakdown: Record<string, number>
  assignment_breakdown: DeviceAssignmentBreakdown
}

interface DeviceAssignmentBreakdown {
  assigned_to_unit: number
  assigned_to_location: number
  assigned_to_employee: number
}

DeviceStatusSnapshot

interface DeviceStatusSnapshot {
  status: string
  last_seen: Date | null
  is_online: boolean | undefined
}

Call

interface Call {
  call_id: string
  device_id: string | null
  duration_seconds: number
  file_size_bytes: number
  storage_format: string
  bitrate: string
  created_at: Date
  download_url: string | null
  s3_path: string | null
  start_timestamp: Date | null
  end_timestamp: Date | null
  call_sequence: number | null
}

CallListResponse

interface CallListResponse {
  device_id: string
  calls: Call[]
  total: number
  page: number
  per_page: number
}

CallStatistics

interface CallStatistics {
  total_calls: number
  total_duration_seconds: number
  total_size_bytes: number
  avg_duration_seconds: number
  first_call: Date | null
  latest_call: Date | null
}

Exceptions

Every non-2xx response throws a typed error in the RaclipError tree:

RaclipError
├── APIConnectionError       # network failure, DNS, TLS, etc.
├── APIError                 # 5xx from the gateway
├── AuthenticationError      # 401
├── PermissionError          # 403
├── NotFoundError            # 404
├── ValidationError          # 400 / 422
└── RateLimitError           # 429 — exposes .retryAfter

All errors extend RaclipError and expose:

Property Type Description
message string Human-readable error message.
statusCode number \| undefined HTTP status code, if available.
responseBody unknown Raw response body from the API.
detail string Extracted detail field from the response body, or falls back to message.

RateLimitError additionally exposes:

Property Type Description
retryAfter number \| null Seconds to wait before retrying, if provided by the server.