Skip to main content

RingSignals

RingSignals connects Ring doorbells and cameras to an agent. It receives Ring webhooks, delivers each event to an agent thread as a notification signal, and adds ring_snapshot and ring_clip tools so the agent can fetch media for an event. It also serves the routes Ring calls for account linking and webhooks.

For setup in the Ring Developer Console and on ring.com, see the Ring integration guide.

Usage example
Direct link to Usage example

src/mastra/agents/home-agent.ts
import { Agent } from '@mastra/core/agent'
import { Memory } from '@mastra/memory'
import { RingSignals } from '@mastra/ring-signals'

export const homeAgent = new Agent({
id: 'home-agent',
name: 'Home Agent',
instructions: 'Watch the front door and say briefly whether each event needs attention.',
model: 'openai/gpt-6.1-sol',
memory: new Memory(),
signals: [
new RingSignals({
events: {
device_offline: { priority: 'low', summary: 'Device went offline.', coalesce: true },
},
tools: {
clip: { visibleToModel: true },
},
}),
],
})

Linked accounts are saved in the channels storage domain. The Mastra instance needs a storage adapter.

Constructor parameters
Direct link to Constructor parameters

All fields are optional. Credentials fall back to environment variables. If clientId, clientSecret, or hmacKey is missing, a warning is logged when the agent is registered, and account linking, webhooks, and media requests fail.

clientId?:

string
= process.env.RING_CLIENT_ID
Client ID of your app in the Ring Developer Console.

clientSecret?:

string
= process.env.RING_CLIENT_SECRET
Client secret of your app in the Ring Developer Console.

hmacKey?:

string
= process.env.RING_HMAC_KEY
HMAC signature key of your Ring app. Verifies link nonces and webhook signatures, and derives the key that signs media links.

baseUrl?:

string
= process.env.RING_BASE_URL
Public URL of your Mastra server, used to build signed snapshot and clip links. Without it, the tools return no links. The agent still sees snapshots, and chat channels still receive attachments.

encryptionKey?:

string
= process.env.MASTRA_ENCRYPTION_KEY
Key used to encrypt stored Ring tokens. When unset, tokens are stored unencrypted and a warning is logged.

mediaUrlTtlMs?:

number
= 604800000 (7 days)
How long signed snapshot and clip links stay valid, in milliseconds.

routePrefix?:

string
= '/ring'
Path prefix for the Ring routes added to the server.

userAgent?:

string
= 'MastraRingSignals/1.0'
User-Agent header sent to the Ring API.

hardwareId?:

string
Hardware_Id sent to the Ring API. By default, a UUID is generated when an account is linked and stored with that account's tokens.

target?:

SignalProviderTarget | RingTargetResolver
= { resourceId: <agent id>, threadId: <device id> }
Where events are delivered when no thread has subscribed to the device with subscribeThread(). Pass a fixed target, or a function that receives the RingWebhookEvent and returns a target, or undefined to drop the event. The function can be async.

events?:

Partial<Record<RingEventType, RingEventNotification | false>>
= DEFAULT_RING_EVENTS
How each Ring event type becomes a notification. Entries are merged over the defaults. Set an event to false to disable it. See Events.
RingEventNotification

priority:

'urgent' | 'high' | 'medium' | 'low'
Notification priority.

summary:

string
Short text the model sees for the event.

coalesce?:

boolean
Merge repeats of this event from the same device into one pending notification, counted by coalescedCount.

attributes?:

(event: RingWebhookEvent) => Record<string, unknown>
Extra attributes the model sees on the notification, merged over the defaults. Values that aren't strings, numbers, booleans, or null are dropped.

instructions?:

string | false
= buildRingInstructions() for the enabled tools
Fixed text added to the agent's system prompt that tells it how to handle Ring notifications. It doesn't change between requests, so it doesn't invalidate the prompt cache. Set to false to leave it out.

tools?:

RingToolsOptions
Which tools the agent gets, and their options. Both tools are enabled by default. Set a tool to false to remove it, or to an options object to configure it. See Tools.
RingToolsOptions

snapshot?:

boolean | RingSnapshotToolOptions
Adds ring_snapshot. Set to false to remove it.

clip?:

boolean | RingClipToolOptions
Adds ring_clip. Set to false to remove it.

Events
Direct link to Events

Ring sends these event types. Events without an entry in DEFAULT_RING_EVENTS are disabled until you configure them in events.

EventSent whenDefault priorityDefault summary
button_pressThe doorbell button is pressedhighDoorbell was pressed.
motion_detectedA device camera detects motionmediumMotion was detected.
device_addedA device becomes accessible to your appDisabled
device_removedA device is no longer accessible to your appDisabled
device_onlineA device comes onlineDisabled
device_offlineA device goes offlineDisabled
app_integration_addedA Ring account links your appDisabled
app_integration_removedA Ring account unlinks your appDisabled
subscription_activatedA Ring paid subscription or trial startsDisabled
subscription_deactivatedA Ring paid subscription or trial endsDisabled

Device events have deviceId and timestamp attributes. Subscription events have no timestamp, and app integration and subscription events have no deviceId. Motion events also have motionType: human, vehicle, package_delivery, or other_motion when Ring Smart Alerts are on, and motion when they're off.

Some events also have side effects, whether or not they're enabled. app_integration_removed deletes the account's stored tokens, and subscription_deactivated logs a warning that media is no longer available.

RingWebhookEvent
Direct link to ringwebhookevent

The webhook payload Ring sends for each event. The target function and the attributes function in events receive it.

type RingWebhookEvent = {
meta: {
version: string
time: string
request_id: string
account_id?: string
}
data: {
id: string
type: RingEventType
attributes: {
source: string
source_id?: string
source_type?: string
timestamp?: number
sub_type?: RingMotionType | RingSubscriptionType
plan_id?: string | null
expires_at?: string
component_ids?: string[]
}
}
}
  • meta.account_id: The Ring account associated with the event.
  • data.type: The event type, from the table above.
  • data.attributes.source: The device ID for device events, the account ID for app_integration_added and app_integration_removed, or subscriptions for subscription events.
  • data.attributes.source_id: The subscription ID, on subscription events.
  • data.attributes.source_type: The kind of source: devices for device events, users for app integration events.
  • data.attributes.timestamp: When the event happened, in epoch milliseconds. It's missing on subscription events.
  • data.attributes.sub_type: What caused a motion event: human, vehicle, package_delivery, other_motion, or motion when Smart Alerts are off. paid or trial on subscription events.
  • data.attributes.plan_id: The subscription plan ID, on subscription events. null for trials.
  • data.attributes.expires_at: When the billing cycle or trial ends, as an ISO 8601 date. Always present on subscription_activated.
  • data.attributes.component_ids: The cameras involved, on devices with more than one.

Tools
Direct link to Tools

ring_snapshot
Direct link to ring_snapshot

Fetches a still image for an event. If Ring has no image right after the event, it uses the most recent image before it, up to 24 hours back.

Input: { deviceId: string, timestamp: number }, taken from the notification's attributes.

attachment?:

boolean | string[]
= true
Send the snapshot as a file attachment on channel threads. true attaches on every channel platform, false never attaches, and a list of platform names attaches only on those. Studio, web, and API threads never get attachments.

maxAttachmentBytes?:

number | Record<string, number>
= 10000000 (10 MB)
Largest snapshot to attach. A number applies to every platform. An object sets a limit per platform name, and unlisted platforms use the default.

visibleToModel?:

boolean
= true
Pass the image to the model so the agent can describe it. Requires a model that supports image input.

ring_clip
Direct link to ring_clip

Fetches a video clip that starts at the event. Ring needs time to process a recording. The tool retries until the clip is ready or processingTimeoutMs passes.

Input: { deviceId: string, timestamp: number }, taken from the notification's attributes.

attachment?:

boolean | string[]
= true
Send the clip as a file attachment on channel threads. Same values as the snapshot option.

maxAttachmentBytes?:

number | Record<string, number>
= 10000000 (10 MB)
Largest clip to attach. Same values as the snapshot option.

visibleToModel?:

boolean
= false
Pass the video to the model so the agent can describe it. Requires a model that supports video input, for example Gemini. Video input counts toward the context window for every second of footage.

audio?:

boolean
= true
Include audio in the clip.

durationMs?:

number
= 15000 (15 seconds)
Clip length in milliseconds. Capped at Ring's maximum of 15 minutes.

processingTimeoutMs?:

number
= 120000 (2 minutes)
How long to wait for Ring to finish processing the recording.

Tool result
Direct link to Tool result

Both tools return the same shape:

mediaId:

string
Identifier for the snapshot or clip.

url?:

string
Signed link to the media on your server. Only present when baseUrl is set.

attached:

boolean
Whether the media was sent as a file attachment on the thread.

Routes
Direct link to Routes

These routes are added to the Mastra server when the agent is registered. They don't use Mastra authentication. Webhooks are verified with Ring's signature, account links with Ring's link nonce, and media links with their signature and expiry time. The token route has no check of its own: it sends each code to Ring, and Ring rejects codes it didn't issue. Rate-limit <routePrefix>/token in your server's middleware.

Each instance needs its own routePrefix. When two instances use the same prefix, Mastra registers only the first instance's routes and logs a warning. The second instance gets no Ring events.

RoutePurpose
GET <routePrefix>/linkAccount Link URL. Finishes linking and shows the result.
GET <routePrefix>/homeApp Homepage URL. Returns Ring Signals as plain text.
POST <routePrefix>/tokenToken Exchange URL. Exchanges an authorization code for tokens.
POST <routePrefix>/webhookWebhook URL. Receives Ring events.
GET <routePrefix>/media/:kind/:deviceId/:timestampServes signed snapshot and clip links.

Methods
Direct link to Methods

subscribeThread(target, deviceId)
Direct link to subscribethreadtarget-deviceid

Sends events from a Ring device to a specific thread. Devices without a subscription use the target option. Several threads can subscribe to the same device, and each one receives every event. Subscriptions are kept in memory and are lost when the server restarts.

ringSignals.subscribeThread({ resourceId: 'user-1', threadId: 'front-door' }, deviceId)

Returns: SignalSubscription

target:

SignalProviderTarget
The thread to deliver events to. Must include threadId and resourceId.

deviceId:

string
The Ring device ID.

unsubscribeThread(target, deviceId)
Direct link to unsubscribethreadtarget-deviceid

Stops sending events from a Ring device to a thread.

Returns: boolean, true if the thread was subscribed.

Exports
Direct link to Exports

@mastra/ring-signals also exports:

  • DEFAULT_RING_EVENTS: The default events configuration.
  • DEFAULT_RING_INSTRUCTIONS: The default system instruction, for both tools.
  • buildRingInstructions({ snapshot, clip }): Builds the system instruction for the enabled tools. Use it to extend the default text.
  • RingApiClient and RingApiError: A client for the Ring Partner API and the error it throws.
  • Types: RingSignalsOptions, RingEventType, RingEventNotification, RingMotionType, RingSubscriptionType, RingWebhookEvent, RingTargetResolver, RingToolsOptions, RingSnapshotToolOptions, RingClipToolOptions, RingAttachmentOption, and RingMaxAttachmentBytesOption.