GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing developer.amazon.com (truncated) — Ring Partner API Documentation | Ring Developer Experience Documentation Alexa Amazon Appstore Ring AWS Documentation Support Contact Us My Cases Ring
Ring Partner API DocumentationOverviewRing provides device capabilities to partners while ensuring a high bar for security, transparency, and control for users. This API enables partners to:
Key Features🔐 Secure AuthenticationOAuth 2.0 with account linking ensures user consent and secure token management 📱 Device DiscoveryComprehensive device discovery and configuration through JSON:API specification 🔔 Real-time NotificationsWebhook-based event system for motion detection and device lifecycle events 📹 Video StreamingWebRTC-based live video with WHEP protocol for low-latency streaming Warning: All requests to the Amazon Vision API (https://api.amazonvision.com) and the Ring OAuth server (https://oauth.ring.com) must be made server-to-server from your Partner App Backend. Browser-initiated requests (e.g., from JavaScript running in the user's browser) will be blocked by CORS policies on Ring's endpoints.
API StandardsThe Ring Partner API follows these standards:
Getting Started Steps
Quick Links
API ReferenceBase URLAll API requests should be made to:
AuthenticationAll API requests require authentication using OAuth 2.0 Bearer tokens: Rate Limiting
Rate Limit Headers
When you exceed the rate limit, the API returns HTTP 429 Too Many Requests. Use exponential backoff starting at 1 second and respect the Content Types
Required HTTP HeadersAll requests to Ring API endpoints (
Requests that are missing required headers may receive a 406 Not Acceptable response. This status code indicates that Ring's web application firewall has blocked the request. Example request with required headers: Important: If your backend server makes requests from a datacenter or cloud-hosting IP address, you may also receive 406 responses due to network-level security rules, even with the correct headers. Contact Ring developer support with your static egress IP address to resolve this.
Authentication EndpointsExchange Authorization CodeRequest: Response: Refresh Access TokenRequest Response: Endpoint ReferenceAll device and media endpoints require On a multi-camera device such as Ring Elite, several endpoints accept an optional Device Endpoints
Media Endpoints
Account & Integration Endpoints
Webhook EventsPartners receive webhook notifications at their configured endpoint. All webhooks include an HMAC-SHA256 signature in the Event Types
Error ResponsesAll errors follow JSON:API format. See Error Handling for comprehensive details. Common Error Codes
Testing Your IntegrationRecommended ToolsThere is no official Ring Partner SDK. Use standard HTTP libraries and tools:
Testing with curlSDKs and LibrariesJavaScript/TypeScriptPythonApplication DeploymentThis document defines the end-to-end deployment architecture for a partner application integrating with the Ring AppStore. It covers the complete journey from initial app creation and credential setup through endpoint configuration, authentication orchestration, device access, and webhook event delivery. 1. Prerequisites and SetupBefore deploying your application, you must create your app in the Ring Developer Portal, obtain credentials, configure your endpoints, and prepare your backend services. 1.1 Create Your ApplicationRegister your application in the Ring Developer Portal. See Configure Your Ring Application for the complete step-by-step registration process.
1.2 Obtain CredentialsWhen you create your application, Ring issues three credentials. These are shown only once — store them securely before continuing.
These credentials are shared across staging and production environments. Never commit them to version control — use environment variables or a secrets manager. There's no way to regenerate credentials on an existing app. If you lose them, or you need to replace them after an exposure, delete the app and create a new one, which issues a fresh set of all three. Use this approach only before you submit the app for certification; after submission, contact Ring support. See Replacing Your App Credentials. 1.3 Configure EndpointsRegister the following HTTPS endpoints in the Ring Developer Portal:
Configure staging endpoints first, validate your integration, then configure production endpoints. See Configure Your Ring Application for detailed requirements. 1.4 Set Up Your BackendBefore a Ring user installs your app, your backend must be ready to handle:
2. App Deployment Journey2.1 Lifecycle Overview2.2 Phase Summary
3. Service Architecture OverviewThe Ring AppStore follows a one-way account linking model where Ring manages the OAuth credential lifecycle and partners verify messages through HMAC digital signatures. Partners do not need to operate as OAuth servers. 3.1 SecurityRing takes security seriously. Our account linking mechanism ensures security through cryptographic verification, time-bound validation, and mandatory user authentication. All communications use HTTPS, and access tokens are scoped to specific devices and operations authorized by the user. 3.2 High-Level Service Architecture3.3 Component Responsibilities
4. Endpoint ConfigurationPartners must configure endpoint categories to complete an integration. These are registered via the Ring Developer Portal during app creation. 4.1 Partner-Hosted Endpoints
4.2 Ring-Hosted Endpoints — Amazon Vision API
See API Reference for full details. 5. Token Exchange FlowAfter a Ring user installs a partner app, Ring Backend generates an OAuth authorization code and sends it to the Partner App Backend. 5.1 What Triggers This Flow
When the user completes device selection, Ring Backend generates an OAuth authorization code and sends it directly to your Token Exchange URL (backend-to-backend). 5.2 Token Exchange Sequence5.3 Handling the Authorization Code RequestRing sends the authorization code to your Token Exchange URL as a form-encoded POST. Parse the body as form data, not JSON: with JSON parsing,
Read the fields by name rather than assuming a fixed set. Unlike webhooks, this request isn't one you can verify by signature. Its security comes from the code itself: it works once, expires in 60 seconds, and is useless without your client secret. The HMAC nonce in the account linking step then binds the tokens to one Ring user. Handle the request in this order, and return 200 only at the end:
If any step fails, return a non-2xx status. Ring treats a non-2xx response as a failed link and doesn't retry the delivery, so build the handler to succeed on the first call, keep it fast, and make it idempotent. Ring issues a new authorization code every time a user links, including when they link again. You can therefore receive tokens for a Ring Account ID you already hold. Keep the newest pair, and attach it to the partner account that signs in during account linking, which may not be the one linked before. 5.4 Token Lifecycle
5.5 Token RefreshSee Refresh Tokens for implementation guidance. 6. Account LinkingAt this point in the deployment journey the Ring user links their Ring account to your partner account. Ring releases OAuth credentials to your backend upfront, redirects the user to your Account Link URL with an HMAC-signed nonce, and your backend matches that nonce to the correct unclaimed token before calling the App-Integrations API to activate the integration. The full specification for this step — the design rationale, the sequence diagram, the nonce technical reference, and the seven implementation steps — is in One-Way Account Linking, alongside the partner-initiated alternative. 7. App Integration FlowThis section shows the end-to-end app integration experience from two perspectives: a step-by-step walkthrough of the complete account linking flow, and a condensed UX flow diagram that maps the same journey across system components. 7.1 Complete Account Linking FlowThe following flow shows each step a Ring user and your backend go through during account linking, from app discovery to a confirmed integration.
7.2 App Integration UX FlowThe following diagram shows the same integration journey as a sequence across the Ring AppStore, Ring API, Partner App UI, and Partner App Backend. 8. Accessing DevicesOnce the integration reaches awaiting or completed status, the Partner App Backend can access authorized Ring devices through the Ring API. See: Device Discovery · Live Video · Media Clips . Image Snapshots 8.1 Device Access Sequence8.2 Authentication HeaderAll Ring API calls require a valid access token: If the token has expired, refresh it before making API calls. See Refresh Tokens. 9. Webhook Event DeliveryRing Backend delivers real-time event notifications to the Partner App Backend via signed webhooks using HMAC-SHA256 signatures. See: Notifications 9.1 Webhook Delivery Sequence9.2 Event Types
10. Environment Configuration
AuthenticationRing utilizes OAuth 2.0 standard tokens for authentication to ensure secure communication with partner cloud services. The authentication system is based on an "account linking" mechanism that provides user-scoped authentication materials while maintaining customer awareness and control. Note: Two account linking methods — Ring AppStore supports two account linking methods. One-way account linking (Ring-driven) is the recommended method for Ring-initiated flows: Ring releases OAuth credentials upfront, eliminating the need for partners to operate as OAuth servers, and uses an HMAC-based nonce to cryptographically bind each account linking redirect to a specific Ring user — partners match this nonce to claim the correct token. See One-Way Account Linking. Partner-Initiated OAuth 2.0 lets partners start the linking flow from their own platforms using a standard OAuth 2.0 authorization code flow with PKCE — currently available by invitation only. See Partner-Initiated OAuth 2.0 Account Linking.
Account Linking MethodsThe following table shows the key similarities and differences between the two account linking methods.
OverviewThe authentication flow involves:
Key Components
Authentication FlowToken TypesAccess Tokens
Refresh Tokens
Warning: Common Mistake: Refresh tokens cannot be used as Bearer tokens. If you place a refresh token in the
Authorization: Bearer header, API calls will fail with 401 Unauthorized. You must first exchange the refresh token for an access token via POST https://oauth.ring.com/oauth/token. See Refresh Tokens for the exchange flow.Quick Start: If You Already Have a Refresh TokenIf you already have a refresh token (e.g., from a completed account linking flow) and need to start making API calls:
The response will include a new Security Requirements
Sub-pages
One-Way Account LinkingThe one-way account linking model eliminates the need for partners to act as OAuth servers. Ring releases credentials upfront and redirects the user to the partner for account association. For the partner-initiated alternative, see Partner-Initiated OAuth 2.0 Account Linking. For a side-by-side comparison of both methods, see Account Linking Methods in the Authentication section. Note: Reduce drop-off with passwordless account linking — Requiring users to sign in with a password at the account linking step is one of the biggest sources of drop-off. Consider implementing passwordless account linking, which lets users complete the link without a password. See Reducing Account Linking Drop-Off in the UX Design Guide for implementation guidance.
See also: App Integrations API Why One-Way Account Linking Is Designed the Way It IsThe one-way account linking flow solves a specific problem: how do you securely associate a Ring user's OAuth tokens with the correct partner user, when the tokens arrive on the backend before the partner even knows who the user is? The Core ChallengeRing wants to release OAuth credentials upfront — before the user logs into the partner service. This means the partner receives tokens for a Ring user but has no idea which partner user they belong to. The tokens are "unclaimed." Why Not Just Send the Ring Account ID in the Redirect?Because the redirect goes through the user's browser. Anything in the URL can be intercepted, modified, or forged. An attacker could swap the Account ID and trick the partner into linking the wrong Ring account to their partner account. There's no way to verify the value is legitimate. Why the HMAC Nonce?The nonce is a cryptographic proof that the redirect came from Ring and corresponds to a specific Ring user. It's computed as Why Does the Partner Need to Call
|
| Property | Value |
|---|---|
| Algorithm | HMAC-SHA256 |
| Validation window | 600 seconds (10 minutes) |
| Output encoding | URL-safe Base64, no padding (RFC 4648 §5, without = characters) |
A-Z, a-z, 0-9, -, _) with no = padding. Standard Base64 (which uses +, /, and = padding) will produce a different string. In Java: Base64.getUrlEncoder().withoutPadding(), in Python: base64.urlsafe_b64encode(...).rstrip(b'=')..hexdigest() + sha256= prefix). Nonces use URL-safe Base64 encoding instead. Do not mix the two.When the user finishes approving data access and selecting devices in the Ring AppStore, Ring Backend generates a cryptographically signed nonce and redirects the user to the partner Account Link URL:
https://partner.example.com/ring/link?nonce=yT8jdW_nu2W4gR6FI-l8hkPpt_c9EAf4DJ9CTIcuM7c&time=1771130906289
The user lands on the Partner App UI login page with the nonce parameters in the URL.
Partner App Backend extracts time and nonce, then performs a freshness check:
VALIDATION_WINDOW_SECONDS = 600 # 10 minutes
current_time_ms = int(time.time() * 1000)
time_param_ms = int(request.args['time'])
time_delta_seconds = (current_time_ms - time_param_ms) / 1000
if time_delta_seconds > VALIDATION_WINDOW_SECONDS:
raise Exception('Link request expired')
if time_delta_seconds < 0:
raise Exception('Invalid timestamp: cannot be in the future')
The user signs in to the Partner App UI. The Partner App Backend authenticates the user and obtains their partner-side account information.
Why sign-in is mandatory:
account_identifier — the signed-in user's email (masked) is used when calling the App-Integrations APIimport hmac
import hashlib
import base64
def compute_nonce(time_param, account_id, hmac_key):
"""Compute nonce using the same algorithm as Ring Backend."""
payload = f"{time_param}:{account_id}"
mac = hmac.new(
hmac_key.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).digest()
return base64.urlsafe_b64encode(mac).rstrip(b'=').decode('utf-8')
def match_nonce_to_token(received_nonce, time_param, hmac_key):
unclaimed_tokens = get_unclaimed_tokens()
for token_record in unclaimed_tokens:
account_id = token_record.account_id
computed_nonce = compute_nonce(time_param, account_id, hmac_key)
# Constant-time comparison to prevent timing attacks
if hmac.compare_digest(computed_nonce, received_nonce):
return token_record # Match found
return None # No match
POST https://api.amazonvision.com/v1/accounts/me/app-integrations
Authorization: Bearer <matched_ava_token>
Content-Type: application/json
{
"account_identifier": "u***[email protected]",
"nonce": "yT8jdW_nu2W4gR6FI-l8hkPpt_c9EAf4DJ9CTIcuM7c"
}
See App Integrations API for full endpoint details.
Ring Backend verifies the nonce, transitions status to awaiting, activates device-level consents, and sends a confirmation email to the Ring user. Ring also sends an app_integration_added webhook and one device_added webhook for each shared device. These can arrive before your POST call returns.
PATCH https://api.amazonvision.com/v1/accounts/me/app-integrations
Authorization: Bearer <matched_ava_token>
Content-Type: application/json
{
"status": "completed"
}
status: completed, the integration remains in awaiting state and is not fully operational. Required flow: POST (nonce verification → awaiting) → PATCH (status update → completed).The nonce-based account linking mechanism provides protection against common attack vectors including replay attacks, token interception, and unauthorized token claiming. The time-bound validation window and cryptographic verification ensure that only legitimate linking attempts succeed.
This section describes Ring AppStore's Partner-Initiated OAuth 2.0 Account Linking — an account linking method that enables partners to start the linking flow from their own platforms. With this method, your app presents a "Connect Ring Account" button; the user is redirected to Ring for login, consent, and device selection; and Ring redirects the user back to your app with a standard OAuth 2.0 authorization code.
This is a standard OAuth 2.0 authorization code flow with PKCE (S256). If you have implemented OAuth 2.0 before, the mechanics are familiar.
For the Ring-driven alternative, see One-Way Account Linking. For a side-by-side comparison of both methods, see Account Linking Methods in the Authentication section.
Availability: Partner-Initiated OAuth 2.0 is currently available by invitation only. Contact your Ring AppStore partner representative to request access.
This section uses the following terms consistently:
| Term | Meaning |
|---|---|
| Ring base plan | A Ring subscription that activates cloud features (video history, snapshots) on Ring devices. Users must have an active Ring base plan on a device before sharing it with a partner app. This is a prerequisite for device eligibility — it is not related to whether the partner app charges users. |
| Free app | An app listed on Ring AppStore that is free for users — no payment required to use it. All Partner-Initiated OAuth 2.0 apps are free apps in the current phase. |
| Paid app | An app listed on Ring AppStore that charges users a fee to use it. Paid app support for Partner-Initiated OAuth 2.0 is planned for a future phase. |
| Subscription activation | The step in the account linking flow where a user subscribes to a paid app. This step is skipped for free apps. Not applicable to Partner-Initiated OAuth 2.0 in the current phase. |
| Billing setup | The process where the developer sets the app price working with a Ring onbarding manager. |
| Account linking | The process of connecting a Ring user's account with a partner service so the partner can access authorized Ring devices via API. |
| PKCE (S256) | Proof Key for Code Exchange — a security extension to OAuth 2.0 that prevents authorization code interception. Mandatory for Partner-Initiated OAuth 2.0. |
Terminology note: "Ring base plan" is unrelated to whether an app is free or paid. A Ring base plan is what users pay Ring for cloud features on their devices — it determines which devices are eligible for sharing. Whether users pay the partner to use the app is a separate concept (free app vs. paid app). A user needs a Ring base plan on their devices regardless of whether the partner app is free or paid.
The following diagram illustrates the Partner-Initiated OAuth 2.0 flow. The initiating participant is on the left, so reading left to right follows the flow.
Key characteristics:
PATCH /v1/accounts/me/app-integrationsFor the one-way (Ring-driven) flow diagram, see One-Way Account Linking.
Before implementing Partner-Initiated OAuth 2.0 account linking, confirm that:
client_id, client_secret, and hmac_signing_key — issued during onboarding via the Ring Developer Portal.Partner-Initiated OAuth 2.0 is gated behind an internal allowlist. To request access and have your app shown as Free, open a support case, select Type of Inquiry: Ring Appstore, and include the following information:
| Information needed | How to obtain it |
|---|---|
| Vendor ID | Sign in at https://developer.amazon.com/settings/console/mycid |
| App ID | Sign in at https://developer.amazon.com/ring/console/apps then navigate to your app and provide the key in the URL on screen, which is between apps and overview |
After allowlisting is complete, the Partner-Initiated OAuth 2.0 configuration fields become available on the Account Linking page of your app in the Developer Portal.
After your app is allowlisted, configure the account linking settings in the Ring Developer Portal.
Navigate to your app → Account linking. The page has separate Staging settings and Production settings tabs — configure both. Under Authorization, provide the following:
| Field | Description | Required |
|---|---|---|
| OAuth Redirect URI | Pre-registered HTTPS URIs that Ring redirects the user to after authorization, with the authorization code. Must exactly match the redirect_uri parameter in the authorization request. |
Yes |
| Account Link URL | Where account linking starts. This is the page on your platform that begins the flow by redirecting the user to Ring's authorization endpoint. If a user starts from your Ring AppStore listing instead, Ring forwards them here. | Yes |
| App Homepage URL | A single link that handles routing for all users — for example, your web app or a smart routing link. Use this when you need one entry point that routes users to the right destination across platforms. | Yes |
| Webhook URL | Your endpoint for receiving Ring event notifications. Ring sends POST requests for events like motion detection, device additions, and device removals. | Yes |
All four values must use HTTPS.
Webhook URL is required even if you plan to poll. You must register an endpoint that returns
200. If you aren't ready to process webhooks at launch, acknowledge deliveries with200and use the Event History API instead — see API polling alternative.
Configure each environment on its own tab:
Save each tab separately. Values do not carry over between environments, so register the redirect URIs for each environment where your app runs.
Everything outside of account linking configuration is unchanged:
This section walks through the complete implementation of Partner-Initiated OAuth 2.0 account linking.
Before redirecting the user, generate a PKCE code verifier and code challenge.
import secrets
import hashlib
import base64
def generate_pkce():
"""Generate PKCE code_verifier and code_challenge (S256)."""
# Generate a random code verifier (43-128 characters)
code_verifier = secrets.token_urlsafe(32)
# Compute SHA-256 hash and Base64URL-encode it (no padding)
digest = hashlib.sha256(code_verifier.encode("ascii")).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
return code_verifier, code_challenge
Store code_verifier securely in your session — you need it for the token exchange in Step 4.
This is the work your Account Link URL performs. Users reach that page either by choosing "Connect Ring Account" in your app or by being forwarded from your Ring AppStore listing. Either way, the page redirects the user's browser to Ring's authorization endpoint with the following parameters:
https://account.ring.com/account/integrations/partner-link/authorize
?client_id=<your_client_id>
&redirect_uri=<your_registered_redirect_uri>
&response_type=code
&scope=<approved_scope>
&state=<random_csrf_token>
&code_challenge=<code_challenge>
&code_challenge_method=S256
| Parameter | Required | Description |
|---|---|---|
client_id |
Yes | Your Ring OAuth client ID (issued during onboarding) |
redirect_uri |
Yes | One of your pre-registered redirect URIs — must exactly match |
response_type |
Yes | Always code |
scope |
Yes | Only the scope ava.v1:read is supported currently |
state |
Yes | An opaque, random string for CSRF protection. Store it in your session and validate it on the callback. |
code_challenge |
Yes | Base64URL-encoded SHA-256 hash of your code_verifier |
code_challenge_method |
Yes | Always S256 |
from flask import redirect, session
import secrets
@app.route("/connect-ring")
def connect_ring():
# Generate PKCE pair
code_verifier, code_challenge = generate_pkce()
# Generate CSRF state token
state = secrets.token_urlsafe(16)
# Store both in session for later validation
session["pkce_code_verifier"] = code_verifier
session["oauth_state"] = state
# Build the authorization URL
auth_url = (
"https://account.ring.com/account/integrations/partner-link/authorize"
f"?client_id={CLIENT_ID}"
f"&redirect_uri={REDIRECT_URI}"
f"&response_type=code"
f"&scope=ava.v1:read"
f"&state={state}"
f"&code_challenge={code_challenge}"
f"&code_challenge_method=S256"
)
return redirect(auth_url)
After you redirect the user:
redirect_uri with ?code=<authorization_code>&state=<your_state>Ring base plan requirement: Users can only share devices that have an active Ring base plan. If no devices are covered, the user is informed they need a Ring base plan and the flow cannot proceed. The user must return to your app and re-initiate after purchasing a plan.
When Ring redirects the user back to your redirect_uri, validate the response and extract the authorization code.
https://your-app.example.com/callback?code=<authorization_code>&state=<your_state>
If the user cancels or an error occurs, Ring redirects with an OAuth error:
https://your-app.example.com/callback?error=<error_code>&error_description=<description>&state=<your_state>
| Error code | Description |
|---|---|
access_denied |
User cancelled the flow, denied consent, or has no devices with an active Ring base plan. Check error_description to tell these apart. |
server_error |
An unexpected error occurred on Ring's side |
invalid_scope |
Requested scope is not approved for your app |
For the full list of error_description values and how to handle each one, see Error Handling Reference.
@app.route("/callback")
def oauth_callback():
# Validate state to prevent CSRF
received_state = request.args.get("state")
if received_state != session.get("oauth_state"):
return "Invalid state parameter", 403
# Check for errors
error = request.args.get("error")
if error:
error_description = request.args.get("error_description", "")
return f"Authorization failed: {error} — {error_description}", 400
# Extract the authorization code
authorization_code = request.args.get("code")
if not authorization_code:
return "Missing authorization code", 400
# Proceed to token exchange (Step 4)
tokens = exchange_code_for_tokens(authorization_code)
return handle_successful_linking(tokens)
Exchange the authorization code at Ring's token endpoint. You must include the code_verifier for PKCE validation.
POST https://oauth.ring.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
code=<authorization_code>
code_verifier=<code_verifier>
client_id=<your_client_id>
client_secret=<your_client_secret>
| Parameter | Required | Description |
|---|---|---|
grant_type |
Yes | Always authorization_code |
code |
Yes | The authorization code received in the callback |
code_verifier |
Yes | The PKCE code verifier you stored in Step 1 |
client_id |
Yes | Your Ring OAuth client ID |
client_secret |
Yes | Your Ring OAuth client secret |
{
"access_token": "xxxxx",
"refresh_token": "yyyyy",
"scope": "ava.v1:read",
"expires_in": 14400,
"token_type": "Bearer"
}
| Field | Description |
|---|---|
access_token |
Bearer token for Ring API calls (valid ~4 hours) |
refresh_token |
Used to obtain new access tokens (valid ~30 days) |
expires_in |
Access token lifetime in seconds |
token_type |
Always Bearer |
import time
import requests
def exchange_code_for_tokens(authorization_code):
"""Exchange the authorization code for access and refresh tokens."""
code_verifier = session.pop("pkce_code_verifier")
response = requests.post(
"https://oauth.ring.com/oauth/token",
data={
"grant_type": "authorization_code",
"code": authorization_code,
"code_verifier": code_verifier,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
if response.status_code != 200:
raise Exception(f"Token exchange failed: {response.text}")
tokens = response.json()
# Store tokens securely — associated with the authenticated partner user
store_user_tokens(
partner_user_id=get_current_user_id(),
access_token=tokens["access_token"],
refresh_token=tokens["refresh_token"],
expires_at=time.time() + tokens["expires_in"],
)
return tokens
Important: The authorization code is single-use and expires within 10 minutes. Exchange it immediately after receiving it. Note that this differs from one-way account linking, where the authorization code expires after 60 seconds.
After obtaining tokens, call the App Integrations API to finalize the account link. This step transitions the integration to a fully active state.
PATCH https://api.amazonvision.com/v1/accounts/me/app-integrations
Authorization: Bearer <access_token>
Content-Type: application/json
{
"status": "completed",
"account_identifier": "u***[email protected]"
}
| Field | Type | Required | Description |
|---|---|---|---|
status |
string | Yes | Set to "completed" to finalize the link. The same endpoint also accepts "awaiting" later to pause an active integration — see Pausing the integration. |
account_identifier |
string | Yes | An obfuscated identifier for the partner user account (e.g., masked email). Displayed to the Ring user for confirmation. |
{
"account_identifier": "u***[email protected]",
"status": "completed",
"updated_at": "2026-07-28T14:30:00Z"
}
import requests
def confirm_account_link(access_token, partner_email):
"""Confirm the account link with Ring — finalizes the integration."""
response = requests.patch(
"https://api.amazonvision.com/v1/accounts/me/app-integrations",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
},
json={
"status": "completed",
"account_identifier": mask_email(partner_email),
},
)
if response.status_code != 200:
raise Exception(f"Account link confirmation failed: {response.text}")
return response.json()
def mask_email(email):
"""Mask email for privacy: u***[email protected]"""
local, domain = email.split("@")
if len(local) <= 2:
masked = local[0] + "***"
else:
masked = local[0] + "***" + local[-1]
return f"{masked}@{domain}"
Important: The integration is not fully active until you call this endpoint with
status: "completed". Without this call, device consents remain inactive and you cannot receive webhook notifications.
Note: Unlike one-way account linking, no
POSTcall with a nonce is required — Partner-Initiated OAuth 2.0 uses only thePATCHcall. See App Integration for the full endpoint reference.
After confirmation, you can call Ring APIs using the access token:
import requests
def get_devices(access_token):
"""List all Ring devices shared with your app."""
response = requests.get(
"https://api.amazonvision.com/v1/devices?include=status,capabilities",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
},
)
return response.json()
For the full API reference, see API Reference.
Token lifetimes and the refresh flow are identical to one-way account linking, with one exception — the authorization code lifetime:
| Token | Lifetime | Purpose |
|---|---|---|
| Authorization code | 10 minutes (single-use) | Exchanged for access and refresh tokens |
| Access token | ~4 hours (14400 seconds) | Bearer token for Ring API calls |
| Refresh token | ~30 days | Used to obtain new access/refresh token pairs |
Refresh tokens proactively — each refresh returns a new refresh_token that invalidates the old one, and an expired refresh token requires the user to re-link from scratch. For the complete refresh flow and best practices, see Refresh Tokens and Access Tokens.
After account linking is complete, Ring delivers real-time event notifications to your Webhook URL. Webhook delivery, HMAC-SHA256 signature verification, payload structure, and retry logic are identical to one-way account linking — no difference in how you verify or handle them. See Notifications for full webhook documentation, including signature verification.
If your architecture cannot support inbound webhooks at initial launch, you can use Ring's Event History API to poll for events:
GET https://api.amazonvision.com/v1/history/devices/{device_id}/events
Authorization: Bearer <access_token>
Webhook support via HMAC signing is available whenever you are ready to enable it.
Although Partner-Initiated OAuth 2.0 flows normally start from your app, your app is still listed on Ring AppStore. When a user discovers your app there and starts linking from your listing:
This ensures a single, consistent account linking path regardless of where the user discovers your app.
Device selection changes and permission review are handled by the Ring user within Ring AppStore. Partners have no API to change a user's device access or permissions. Partners can change two things about the integration itself: pause and resume it — see Pausing the integration — and remove it — see Removing the integration.
| Action | Who performs it | How it's done |
|---|---|---|
| Modify device selection | Ring user | Ring AppStore → My Apps → select your app → change devices |
| Review permissions | Ring user | Ring AppStore → My Apps → select your app |
| Remove app / unlink | Ring user or partner | Ring user: Ring AppStore → My Apps → select your app → Remove App. Partner: DELETE /v1/accounts/me/app-integrations |
| Pause or resume the integration | Partner | PATCH /v1/accounts/me/app-integrations with status set to awaiting or completed |
When a user wants to change their device selection or review permissions from within your app, you must navigate them to the Ring AppStore page for your app. There is no API for changing a user's device selection, so don't attempt it from your own UI. To let the user disconnect Ring from within your app, call DELETE /v1/accounts/me/app-integrations — see Removing the integration.
Example implementation:
# When user clicks "Manage Ring Connection" in your app
RING_APPSTORE_MY_APPS_URL = "https://account.ring.com/account/integrations"
def get_manage_ring_url():
"""Return the URL where users manage their Ring integration."""
return RING_APPSTORE_MY_APPS_URL
You can pause an active integration instead of removing it. Call the same PATCH /v1/accounts/me/app-integrations endpoint you used to activate the link, with status set to awaiting instead of completed. This moves the integration back to the awaiting state, which is useful when your service is undergoing maintenance, when you need to suspend event processing temporarily, or when your own configuration for that user is incomplete.
Pausing is reversible. Call the endpoint again with status set to completed to resume. Pausing does not invalidate tokens and does not remove the account link — the user's device authorizations and your stored credentials remain intact.
PATCH https://api.amazonvision.com/v1/accounts/me/app-integrations
Authorization: Bearer <access_token>
Content-Type: application/json
{
"status": "awaiting"
}
| Value | Effect |
|---|---|
awaiting |
Pauses the integration |
completed |
Resumes the integration |
import requests
AVA_BASE_URL = "https://api.amazonvision.com"
def set_integration_status(access_token, status):
"""Pause or resume a Ring integration.
Pass status="awaiting" to pause, status="completed" to resume.
"""
if status not in ("awaiting", "completed"):
raise ValueError("status must be 'awaiting' or 'completed'")
response = requests.patch(
f"{AVA_BASE_URL}/v1/accounts/me/app-integrations",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
},
json={"status": status},
)
if response.status_code != 200:
raise Exception(f"Status update failed: {response.text}")
return response.json()
def pause_integration(access_token):
"""Pause the integration — for example, during maintenance."""
return set_integration_status(access_token, "awaiting")
def resume_integration(access_token):
"""Resume a paused integration."""
return set_integration_status(access_token, "completed")
Use a valid access token. Pausing requires the access token for the specific Ring user whose integration you are pausing. Refresh the token first if it has expired.
To unlink a Ring user from your app, call DELETE on the same /v1/accounts/me/app-integrations endpoint. Ring removes the integration for the Ring user identified by the Bearer token and revokes that user's access and refresh tokens. Use it when the user disconnects Ring from within your app or closes their account with you.
Removal is permanent. The user must go through the account linking flow again to reconnect. If you only need to stop event processing for a while, pause the integration instead.
Unlike POST and PATCH on this endpoint, DELETE takes no request body and no Content-Type header — the target integration comes from the Bearer token.
Availability:
DELETE /v1/accounts/me/app-integrationsworks only for apps that use Partner-Initiated OAuth 2.0. Apps that use one-way (Ring-driven) account linking receive403 Forbidden; their users remove the integration from Ring AppStore.
DELETE https://api.amazonvision.com/v1/accounts/me/app-integrations
Authorization: Bearer <access_token>
{
"data": {
"type": "app-integrations",
"id": "ava1.ring.client.<client_id>",
"attributes": {
"status": "deleted"
}
},
"meta": {
"time": "2026-09-15T14:30:00Z"
}
}
After a successful call, Ring does exactly what it does when a user removes your app from Ring AppStore: it invalidates the user's access and refresh tokens, stops sending webhook events for that user, and sends an app_integration_removed webhook — see Handling removal notifications. Discard the stored tokens as soon as the call returns 200 OK; do not wait for the webhook.
import requests
AVA_BASE_URL = "https://api.amazonvision.com"
def remove_integration(access_token):
"""Unlink the Ring user and revoke their tokens.
Call this when the user disconnects Ring from your app. The access
token must belong to the Ring user being unlinked.
"""
response = requests.delete(
f"{AVA_BASE_URL}/v1/accounts/me/app-integrations",
headers={"Authorization": f"Bearer {access_token}"},
)
if response.status_code == 404:
# Already removed — for example, by the user from Ring AppStore
return None
if response.status_code != 200:
raise Exception(f"Integration removal failed: {response.text}")
return response.json()["data"]["attributes"]
Use a valid access token. Removal requires the access token for the specific Ring user whose integration you are removing. Refresh the token first if it has expired.
When a user removes your app from Ring AppStore, or your app removes the integration with DELETE, Ring:
app_integration_removed webhook (if your endpoint is reachable)Your app should handle 401 Unauthorized responses gracefully as an indicator that the user has removed the integration. Prompt the user to re-link if they want to reconnect.
PKCE prevents authorization code interception attacks. It is mandatory for all Partner-Initiated OAuth 2.0 flows. Ring rejects authorization requests that do not include code_challenge and code_challenge_method=S256.
redirect_uri in the authorization request does not match a registered URI, Ring shows an error page — it does not redirect to the unvalidated URIstate parameter in every authorization requeststate matches what you stored in the user's sessionIf you are already integrated with one-way (Ring-driven) account linking, the following table shows what changes with Partner-Initiated OAuth 2.0.
| Concern | One-Way (Ring-Driven) | Partner-Initiated OAuth 2.0 |
|---|---|---|
| Who starts the flow | Ring — user begins in Ring AppStore | Partner — user begins in your app |
| Token Exchange URL | Required — Ring POSTs the auth code to your backend | Not used — you receive the code via browser redirect |
| Account Link URL | Required — Ring redirects user here with the nonce after the token exchange | Required — the page that starts the flow, and where Ring forwards users who begin from your AppStore listing |
| HMAC nonce matching | Required — you iterate unclaimed tokens and match | Not used — no nonce, no unclaimed token pool |
| Unclaimed token pool | Required — you store tokens as "unclaimed" until nonce match | Not needed — you receive and store tokens directly |
| App Integrations API | POST (nonce verify) + PATCH (status: completed) | PATCH (status: completed) + DELETE (remove the integration) |
| Pausing an integration | PATCH with status: awaiting | PATCH with status: awaiting — identical |
| Removing an integration | Ring user only, from Ring AppStore | Ring user from Ring AppStore, or partner via DELETE |
| Webhooks | HMAC-SHA256 — identical | HMAC-SHA256 — identical |
| Token refresh | Identical | Identical |
| Ring API usage | Identical | Identical |
Use this checklist to verify your integration is complete.
code_verifier + code_challenge with S256)account.ring.com/account/integrations/partner-link/authorizestate parameter generated, stored in session, and validated on callbackstate and checks for error responsesoauth.ring.com/oauth/token with code_verifierPATCH /v1/accounts/me/app-integrations with status: "completed"401 Unauthorized handling — refresh and retry, or prompt re-link if refresh failsapplication/jsonmeta.request_idDELETE /v1/accounts/me/app-integrations and discards the stored tokensThese errors are returned as query parameters on your redirect_uri:
error |
error_description |
Meaning | Recommended action |
|---|---|---|---|
access_denied |
user_cancelled |
User closed the flow or clicked cancel | Show a friendly message; offer to retry |
access_denied |
user_denied_consent |
User explicitly denied consent | Inform the user that permissions are required |
access_denied |
ring_base_plan_required |
No devices with an active Ring base plan | Inform the user they need a Ring base plan; link to https://ring.com/plans |
server_error |
varies | Unexpected error on Ring's side | Show a generic error; offer to retry later |
invalid_scope |
— | Requested scope is not approved for this app | Verify your scope matches what was approved at certification |
| Condition | Behavior |
|---|---|
Invalid or unregistered redirect_uri |
Ring shows an error page. No redirect — unvalidated URIs are never redirected to. |
Unknown client_id |
Ring shows an error page. No redirect. |
| HTTP Status | Cause | Fix |
|---|---|---|
| 400 | Invalid code_verifier, expired code, or code already used |
Ensure code_verifier matches the code_challenge from the authorization request. Authorization codes are single-use. |
| 401 | Invalid client_id or client_secret |
Verify your credentials. |
| HTTP Status | Cause | Fix |
|---|---|---|
| 400 | Missing required fields or invalid status transition (PATCH); malformed request (DELETE) |
Ensure status is "completed" and account_identifier is present. For DELETE, send no body and no Content-Type header |
| 403 | DELETE called by an app that does not use Partner-Initiated OAuth 2.0, or the token is not authorized for this user |
Removal is available to Partner-Initiated OAuth 2.0 apps only. Verify the access token belongs to the Ring user you are unlinking |
| 404 | No integration found for this token | Verify you are using the access token from this linking session. For DELETE, the integration is already gone — treat it as removed |
| 500 | Internal error or downstream failure. For DELETE, this includes downstream throttling — the operation never returns 429 |
Retry with exponential backoff |
Can my app support both one-way and partner-initiated account linking?
Partners can support only one account linking method per app. An app configured for Partner-Initiated OAuth 2.0 uses that method exclusively. If a user discovers the app on Ring AppStore and starts linking there, Ring forwards them to your Account Link URL, which starts the partner-initiated flow.
Do I need a Token Exchange URL?
No. The Token Exchange URL is specific to one-way (Ring-driven) account linking, where Ring POSTs the authorization code to your backend. In Partner-Initiated OAuth 2.0 you receive the authorization code on your registered OAuth Redirect URI via browser redirect, then exchange it yourself at https://oauth.ring.com/oauth/token.
Do I need to implement HMAC nonce matching?
No. The nonce mechanism is specific to one-way account linking. In Partner-Initiated OAuth 2.0, the Bearer token obtained through the code exchange is the proof of possession.
What happens if the user has no Ring base plan?
Users can only share devices covered by an active Ring base plan. If no devices qualify, Ring shows a message explaining the requirement. The flow terminates and the user must return to your app after purchasing a plan to re-initiate.
What if the user is already linked to my app?
One active link per user per app. Ring blocks duplicate linking attempts. The user must remove their existing integration from Ring AppStore before re-linking.
Can I programmatically unlink a user?
Yes. Call DELETE /v1/accounts/me/app-integrations with the user's access token. Ring removes the integration, revokes the user's tokens, and sends an app_integration_removed webhook — see Removing the integration. This works for Partner-Initiated OAuth 2.0 apps only. If you only need to stop event processing temporarily, pause the integration instead.
How do I pause an integration without unlinking the user?
Call PATCH /v1/accounts/me/app-integrations with status set to awaiting. Resume by calling it again with status set to completed. Pausing keeps tokens and device authorizations intact — see Pausing the integration.
What are the token lifetimes?
Access token: ~4 hours. Refresh token: ~30 days. You must refresh tokens proactively — an expired refresh token requires the user to re-link.
Do webhooks work the same way?
Yes. Webhook delivery, HMAC-SHA256 signature verification, payload structure, and retry logic are identical regardless of which account linking method was used. See Notifications.
Refresh tokens enable partners to maintain long-term access to Ring user data by obtaining new access tokens when the current ones expire.
Authorization: Bearer headers. You MUST exchange them for an access token first using the POST https://oauth.ring.com/oauth/token endpoint described below. Placing a refresh token in the Authorization: Bearer header will result in 401 Unauthorized errors.POST https://oauth.ring.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=<your_refresh_token>&client_id=<your_client_id>&client_secret=<your_client_secret>
import requests
OAUTH_TOKEN_URL = "https://oauth.ring.com/oauth/token"
def exchange_refresh_token(refresh_token, client_id, client_secret):
"""Exchange a refresh token for a new access token."""
response = requests.post(OAUTH_TOKEN_URL, data={
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"client_id": client_id,
"client_secret": client_secret,
})
response.raise_for_status()
tokens = response.json()
# tokens contains: access_token, refresh_token, expires_in, token_type, scope
return tokens
# Usage
tokens = exchange_refresh_token(
refresh_token="your_refresh_token_here",
client_id="your_client_id_here",
client_secret="your_client_secret_here",
)
# Now use the access token for API calls
headers = {"Authorization": f"Bearer {tokens['access_token']}"}
async function exchangeRefreshToken(refreshToken, clientId, clientSecret) {
const response = await fetch("https://oauth.ring.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: clientId,
client_secret: clientSecret,
}),
});
if (!response.ok) throw new Error(`Token exchange failed: ${response.status}`);
return await response.json();
}
Ring recommends refreshing tokens in these scenarios:
expires_in value from the token responsePOST https://oauth.ring.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
refresh_token=<refresh_token>
client_id=<client_id>
client_secret=<client_secret>
Response:
{
"access_token": "xxxxx",
"refresh_token": "yyyyy",
"scope": "<scope>",
"expires_in": 14400,
"token_type": "Bearer"
}
To prevent refresh token expiry, implement a background job that refreshes tokens before they expire:
import time
# Recommended: refresh tokens every 24 hours to maintain a healthy margin
REFRESH_INTERVAL_SECONDS = 86400 # 24 hours
def background_token_refresh():
"""Background job to proactively refresh all stored tokens."""
for token_record in get_all_active_tokens():
try:
new_tokens = refresh_access_token(token_record.refresh_token)
update_stored_tokens(token_record.account_id, new_tokens)
except Exception as e:
# Alert on refresh failures — may indicate approaching expiry
alert_token_refresh_failure(token_record.account_id, e)
refresh_token and invalidates the old one. Store the new token every time you refreshAccess tokens are the primary authentication material used to access Ring user data. They are user-scoped, meaning each token is associated with a specific Ring user account.
Authorization: Bearer headers. Refresh tokens are not valid Bearer tokens — using a refresh token in the Authorization header will result in 401 Unauthorized errors. If you only have a refresh token, you must first exchange it for an access token via POST https://oauth.ring.com/oauth/token. See Refresh Tokens for the exchange flow.Include access tokens in the Authorization header for all API requests:
Authorization: Bearer <access_token>
GET https://api.amazonvision.com/v1/devices
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Access tokens include an expires_in field indicating their lifetime in seconds:
{
"access_token": "xxxxx",
"refresh_token": "yyyyy",
"scope": "<scope>",
"expires_in": 14400,
"token_type": "Bearer"
}
import time
import requests
class TokenManager:
def __init__(self, access_token, refresh_token, expires_in):
self.access_token = access_token
self.refresh_token = refresh_token
self.expires_at = time.time() + expires_in
def get_valid_token(self):
if time.time() >= self.expires_at - 300: # Refresh 5 minutes early
self.refresh_token_if_needed()
return self.access_token
def refresh_token_if_needed(self):
# Implement refresh logic here
pass
def make_authenticated_request(self, url):
headers = {
'Authorization': f'Bearer {self.get_valid_token()}'
}
return requests.get(url, headers=headers)
After receiving an access token, partners should call GET https://api.amazonvision.com/v1/users/me to retrieve the Ring user's Account ID. This Account ID is used for nonce matching during account linking and correlating webhook events. See Users API for details.
Access tokens are scoped to specific permissions granted during the account linking process. The scope determines which Ring resources and operations the token can access.
Common scopes include:
The Users API allows partners to retrieve basic profile information about the Ring user associated with their AVA access token. This endpoint returns the user's Account ID, name, email, and phone number.
| Method | Endpoint | Purpose |
|---|---|---|
| GET | https://api.amazonvision.com/v1/users/me |
Get the authenticated user's profile information |
Requires a valid AVA access token:
Authorization: Bearer <ava_token>
See Access Tokens for details on obtaining and managing tokens.
Returns basic profile information for the Ring user associated with the provided AVA token. The me path parameter indicates the currently authenticated user.
GET https://api.amazonvision.com/v1/users/me
Authorization: Bearer <ava_token>
No request body is required.
{
"data": {
"type": "users",
"id": "ava1.ring.account.XXXYYY",
"attributes": {
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]"
}
}
}
| Field | Type | Description |
|---|---|---|
data.type |
string | Always users |
data.id |
string | The user's Account ID — use this to identify the Ring user in your system |
data.attributes.first_name |
string | User's first name |
data.attributes.last_name |
string | User's last name |
data.attributes.email |
string | User's email address |
data.attributes.phone_number |
string | User's phone number |
| Status | Description |
|---|---|
| 400 | Malformed request |
| 403 | Client is not authenticated or token is invalid |
| 404 | No user found for the given token |
| 500 | Internal server error |
The Account ID (data.id) returned by this endpoint is used throughout the Ring integration:
Nonce matching during account linking — The Account ID is the binding identifier in the HMAC nonce. Ring computes the nonce as HMAC-SHA256(K_hmac, "<timestamp_ms>:<account_id>"). Partners call this endpoint for each unclaimed token at receipt time to store the Account ID, then use it to recompute and match the nonce during account linking. See One-Way Account Linking for details.
Webhook event association — The same Account ID appears in the meta.account_id field of all webhook notifications, allowing partners to associate events with the correct Ring user. See Notifications.
User management — Use the Account ID as the stable identifier for Ring users in your system. Avoid indexing on email or name as these may change.
import requests
def get_user_profile(ava_token):
"""Get the Ring user profile for the given AVA token."""
response = requests.get(
"https://api.amazonvision.com/v1/users/me",
headers={
"Authorization": f"Bearer {ava_token}"
}
)
if response.status_code == 200:
user_data = response.json()['data']
return {
"account_id": user_data['id'],
"first_name": user_data['attributes']['first_name'],
"last_name": user_data['attributes']['last_name'],
"email": user_data['attributes']['email'],
"phone_number": user_data['attributes']['phone_number']
}
elif response.status_code == 403:
raise PermissionError("Invalid or expired token")
elif response.status_code == 404:
raise LookupError("User not found")
else:
response.raise_for_status()
Call this endpoint immediately after receiving an AVA token to store the Account ID for later use in nonce matching and event correlation:
def on_token_received(ava_token, refresh_token):
"""Called when a new AVA token is received during OAuth exchange."""
profile = get_user_profile(ava_token)
account_id = profile['account_id']
token_store.save(
access_token=ava_token,
refresh_token=refresh_token,
account_id=account_id,
status="unclaimed"
)
return account_id
The App Integrations API allows partners to manage the lifecycle of their Ring AppStore integration. Partners use these endpoints to confirm account linking, update integration configuration status, and manage the association between their users and Ring users.
The App Integrations API provides two operations on a single endpoint. Both calls are required to complete the integration — POST to confirm the account link, then PATCH to finalize the integration status:
| Order | Method | Endpoint | Purpose |
|---|---|---|---|
| 1 | POST | https://api.amazonvision.com/v1/accounts/me/app-integrations |
Confirm account link — validate the nonce and associate a partner account with a Ring user (status → awaiting) |
| 2 | PATCH | https://api.amazonvision.com/v1/accounts/me/app-integrations |
Required — Update integration status to completed after partner-side configuration is done |
status: completed. Without this call, the integration remains in awaiting state and is not fully operational in the Ring AppStore.Both endpoints:
Authorization: Bearer headerContent-Type: application/json)data object alongside a meta objectAll requests require a valid AVA access token obtained through the OAuth token exchange flow:
Authorization: Bearer <ava_token>
See Access Tokens for details.
Called by the Partner App Backend after a Ring user has authenticated with the partner service. This endpoint verifies the nonce from the account linking redirect and establishes the link between the Ring user and the partner account.
Call this endpoint after:
time and nonce query parametersPOST https://api.amazonvision.com/v1/accounts/me/app-integrations
Authorization: Bearer <ava_token>
Content-Type: application/json
{
"account_identifier": "u***[email protected]",
"nonce": "<nonce_value>"
}
| Field | Type | Required | Description |
|---|---|---|---|
account_identifier |
string | Optional | Obfuscated partner account identifier displayed to the Ring user for confirmation (e.g., masked email u***[email protected]). Must be derived from the user's actual signed-in session. |
nonce |
string | Required | The nonce received from the Ring redirect URL — used to cryptographically verify the linking attempt |
{
"data": {
"type": "app-integrations",
"id": "ava1.ring.client.<client_id>",
"attributes": {
"status": "awaiting"
}
},
"meta": {
"time": "2026-07-21T07:07:15Z"
}
}
| Field | Type | Description |
|---|---|---|
data.type |
string | Always app-integrations |
data.id |
string | Opaque client identifier, prefixed ava1.ring.client. — treat it as a string key |
data.attributes.status |
string | Integration status after verification — awaiting indicates the link is verified and awaiting configuration completion |
meta.time |
string (ISO 8601) | Timestamp of the response |
| Status | Title | Description |
|---|---|---|
| 400 | Invalid Nonce | Nonce validation failed |
| 400 | Missing Required Fields | The nonce field is missing |
| 404 | Not Found | No integration exists for this token |
When this endpoint is called successfully:
awaitingapp_integration_added webhook and one device_added webhook for each shared device, typically within seconds. They can arrive before this call returns.import requests
def verify_account_link(ava_token, account_identifier, nonce):
"""Verify the account link with Ring after user authenticates."""
response = requests.post(
"https://api.amazonvision.com/v1/accounts/me/app-integrations",
headers={
"Authorization": f"Bearer {ava_token}",
"Content-Type": "application/json"
},
json={
"account_identifier": account_identifier,
"nonce": nonce
}
)
if response.status_code == 200:
# Status is nested inside the JSON:API envelope
return response.json()["data"]["attributes"]["status"] # "awaiting"
elif response.status_code == 400:
raise ValueError(f"Verification failed: {response.json()}")
elif response.status_code == 404:
raise LookupError("Integration not found for this token")
else:
response.raise_for_status()
Called by the Partner App Backend to update the integration status. This call is mandatory after the POST nonce validation to finalize the integration.
status: completedaccount_identifierPATCH https://api.amazonvision.com/v1/accounts/me/app-integrations
Authorization: Bearer <ava_token>
Content-Type: application/json
{
"account_identifier": "u***[email protected]",
"status": "completed"
}
| Field | Type | Required | Description |
|---|---|---|---|
account_identifier |
string | Optional | Updated partner account identifier |
status |
string | Optional | completed (configuration done) or awaiting (in progress) |
At least one field must be provided.
{
"data": {
"type": "app-integrations",
"id": "ava1.ring.client.<client_id>",
"attributes": {
"account_identifier": "u***[email protected]",
"status": "completed",
"updated_at": "2026-07-21T07:07:16Z"
}
},
"meta": {
"time": "2026-07-21T07:07:16Z"
}
}
| Field | Type | Description |
|---|---|---|
data.type |
string | Always app-integrations |
data.id |
string | Opaque client identifier, prefixed ava1.ring.client. — treat it as a string key |
data.attributes.account_identifier |
string | The current partner account identifier, returned in the same masked form it was submitted |
data.attributes.status |
string | The current integration status — awaiting or completed |
data.attributes.updated_at |
string (ISO 8601) | Timestamp of this update |
meta.time |
string (ISO 8601) | Timestamp of the response |
| Status | Title | Description |
|---|---|---|
| 400 | Missing Required Fields | Request body is empty |
| 400 | Invalid Status Transition | The requested status change is not allowed |
| 404 | Not Found | No integration exists for this token |
| Status | Meaning | Set By | Required |
|---|---|---|---|
awaiting |
Link verified, device access enabled, awaiting partner configuration | Ring (after POST) or Partner (via PATCH) | POST triggers this automatically |
completed |
Fully configured and operational | Partner (via PATCH) | PATCH is mandatory to reach this state |
Required integration flow:
awaitingstatus: completed → status becomes completed (mandatory)import requests
def complete_integration(ava_token, account_identifier, nonce):
"""Complete the full integration: POST nonce verification, then PATCH to completed."""
# Step 1: POST — Verify the nonce and confirm account link
post_response = requests.post(
"https://api.amazonvision.com/v1/accounts/me/app-integrations",
headers={
"Authorization": f"Bearer {ava_token}",
"Content-Type": "application/json"
},
json={
"account_identifier": account_identifier,
"nonce": nonce
}
)
if post_response.status_code != 200:
raise ValueError(f"POST nonce verification failed: {post_response.json()}")
# Step 2: PATCH — Update status to completed (REQUIRED)
patch_response = requests.patch(
"https://api.amazonvision.com/v1/accounts/me/app-integrations",
headers={
"Authorization": f"Bearer {ava_token}",
"Content-Type": "application/json"
},
json={
"status": "completed"
}
)
if patch_response.status_code != 200:
raise ValueError(f"PATCH status update failed: {patch_response.json()}")
# Attributes are nested inside the JSON:API envelope
return patch_response.json()["data"]["attributes"]
account_identifier must come from an authenticated partner sessionAfter obtaining customer authentication materials, partners can access Ring's public API to discover and interact with shared devices. Device discovery is the entry point for accessing Ring resources.
Device discovery follows the JSON:API Specification and provides:
Retrieve a list of devices accessible to the authenticated user:
GET https://api.amazonvision.com/v1/devices
Authorization: Bearer <access_token>
{
"meta": {
"time": "2026-02-14T22:57:16Z"
},
"data": [
{
"type": "devices",
"id": "ava1.ring.device.XXXYYY",
"attributes": {
"name": "Front Door Camera"
},
"relationships": {
"status": {
"data": {
"type": "device-status",
"id": "ava1.ring.device.status.XXXYYY"
},
"links": {
"related": "/v1/devices/ava1.ring.device.XXXYYY/status"
}
},
"capabilities": {
"data": {
"type": "device-capabilities",
"id": "ava1.ring.device.capabilities.XXXYYY"
},
"links": {
"related": "/v1/devices/ava1.ring.device.XXXYYY/capabilities"
}
},
"configurations": {
"data": {
"type": "device-configurations",
"id": "ava1.ring.device.configurations.XXXYYY"
},
"links": {
"related": "/v1/devices/ava1.ring.device.XXXYYY/configurations"
}
},
"location": {
"data": {
"type": "locations",
"id": "ava1.ring.device.location.XXXYYY"
},
"links": {
"related": "/v1/devices/ava1.ring.device.XXXYYY/location"
}
}
}
}
]
}
| Field | Type | Description |
|---|---|---|
meta.time |
string | ISO 8601 timestamp of the response |
data |
array | Array of device resources |
data[].type |
string | Always "devices" |
data[].id |
string | Unique device identifier (e.g., ava1.ring.device.XXXYYY) — use this in all device-specific API calls |
data[].attributes.name |
string | User-assigned device name (e.g., "Front Door Camera") |
data[].attributes.image_url |
string | URL of a square product image for the device |
data[].relationships |
object | JSON:API resource linkage — each relationship contains data (type + id) and links (API path) |
ava1.ring.device.GPBGLFYKSWL2...). Do not parse or make assumptions about their structure — use them as-is in API paths.Use the include query parameter to fetch related data in a single request:
GET https://api.amazonvision.com/v1/devices?include=status,capabilities
Authorization: Bearer <access_token>
When using ?include=status,capabilities, the related resources are returned in a top-level included array. Match resources using the type + id pair from relationships.{rel}.data to the corresponding entry in included:
{
"meta": {
"time": "2026-02-14T22:57:16Z"
},
"data": [
{
"type": "devices",
"id": "ava1.ring.device.XXXYYY",
"attributes": {
"name": "Office"
},
"relationships": {
"status": {
"data": { "type": "device-status", "id": "ava1.ring.device.status.XXXYYY" },
"links": { "related": "/v1/devices/ava1.ring.device.XXXYYY/status" }
},
"capabilities": {
"data": { "type": "device-capabilities", "id": "ava1.ring.device.capabilities.XXXYYY" },
"links": { "related": "/v1/devices/ava1.ring.device.XXXYYY/capabilities" }
}
}
}
],
"included": [
{
"type": "device-status",
"id": "ava1.ring.device.status.XXXYYY",
"attributes": {
"online": true
}
},
{
"type": "device-capabilities",
"id": "ava1.ring.device.capabilities.XXXYYY",
"attributes": {
"video": {
"configurations": ["resolution_mode"],
"codecs": ["AVC"],
"ratio": "16:9",
"max_resolution": 1080,
"supported_resolutions": [1080]
},
"motion_detection": {
"configurations": ["enabled", "motion_zones"]
},
"image_enhancements": {
"configurations": ["color_night_vision", "hdr", "ir_led_night_vision", "privacy_zones"]
}
}
}
]
}
?include= works: Resources requested via include appear in the top-level included array. Resources NOT requested still appear in relationships with data and links but are not embedded in included — use the links.related path to fetch them individually. You can include any combination: status, capabilities, location, configurations.| Included Type | Key Attributes | Description |
|---|---|---|
device-status |
online (boolean) |
Whether the device is currently online |
device-capabilities |
button_press, video, motion_detection, image_enhancements, components |
Device feature support — doorbell press support, codecs, resolutions, configurable settings. button_press appears on doorbells only, and components on multi-camera devices only. See Capabilities |
device-configurations |
motion_detection, image_enhancements |
Current user settings — zones, enhancements. See Configurations |
locations |
Coarse location data | Country/state-level location. See Locations |
A Ring Elite camera contains several camera modules but returns a single device entry, structurally identical to any other camera. Sideload capabilities with ?include=capabilities and check for a components object in the device-capabilities resource: its presence tells you the device has multiple camera modules and lists them. Devices without components are single-camera devices.
For the full model, see Multi-Camera Devices.
Each device provides relationships to:
The device discovery API follows JSON:API specifications:
type, id, attributes, relationshipsdata objects (type + id) in relationshipsinclude parameter with top-level included arraylinks for individual resource fetchingDevice capabilities provide information about the hardware features and supported functionality of Ring devices. This information helps partners understand what operations are available for each device.
Capabilities can be retrieved in two ways:
include parameter:
GET https://api.amazonvision.com/v1/devices?include=capabilities
Authorization: Bearer <access_token>
GET https://api.amazonvision.com/v1/devices/{device_id}/capabilities
Authorization: Bearer <access_token>
component_id query parameter:
GET https://api.amazonvision.com/v1/devices/{device_id}/capabilities?component_id=5
Authorization: Bearer <access_token>
{
"meta": {
"time": "2025-07-07T10:30:00Z"
},
"data": {
"id": "xxxyyy.capabilities",
"type": "device-capabilities",
"attributes": {
"button_press": {
"supported": true
},
"video": {
"configurations": ["resolution_mode"],
"codecs": ["HEVC"],
"ratio": "16:9",
"max_resolution": 2160,
"supported_resolutions": [2160]
},
"motion_detection": {
"configurations": ["enabled", "motion_zones"]
},
"image_enhancements": {
"configurations": [
"color_night_vision",
"hdr",
"ir_led_night_vision",
"auto_zoom_track",
"privacy_zones"
]
}
},
"relationships": {
"configurations": {
"links": {
"related": "/v1/devices/xxxyyy/configurations"
}
}
}
}
}
true when the device is a doorbell and sends Button Press eventsDoorbells return the button_press object. Other devices don't, so treat a missing button_press object as no doorbell button. Read it at discovery time to choose the experience you offer for a device before any event arrives.
ratio and max_resolution — don't assume them. Both vary by device, and the values in the example above are not universal. A Ring Elite 360 reports a ratio of 19:21 and a max_resolution of 2736. Marketing resolution claims don't map directly to these values either, so always use what the response gives you.Devices with more than one camera module — the Ring Elite lineup — list them under components. The field is absent on single-camera devices.
| Field | Type | Description |
|---|---|---|
components.items[].component_id |
string | Identifier for one camera module. Pass as the component_id query parameter. |
components.items[].component_type |
string | Module type — "lens" for a camera module. |
components.items[].component_name |
string | Display label, such as "View 1". |
The array order matches the physical arrangement of the modules. See Multi-Camera Devices.
Request one camera module's metadata with the component_id query parameter.
GET https://api.amazonvision.com/v1/devices/{device_id}/capabilities?component_id=5
Authorization: Bearer <access_token>
{
"data": {
"type": "component-capabilities",
"id": "ava1.ring.component.capabilities.XXXYYY",
"attributes": {
"component_type": "lens",
"component_id": "5",
"component_name": "View 6"
},
"relationships": {
"configurations": {
"links": {
"related": "/v1/devices/{device_id}/configurations?component_id=5"
}
}
}
},
"meta": {
"time": "2026-08-14T13:42:45Z"
}
}
The scoped response returns identity metadata only — component_type, component_id, and component_name. Video, motion detection, and image enhancement capabilities aren't scoped per component. Read those from the device-level response, which describes the first component in components.items.
Note the resource type is component-capabilities (not device-capabilities) and the id uses the ava1.ring.component.capabilities. prefix.
Partners should:
configurations array to understand available settingsdef check_video_capabilities(device_capabilities):
video_caps = device_capabilities.get('attributes', {}).get('video')
if video_caps:
max_res = video_caps.get('max_resolution', 1080)
codecs = video_caps.get('codecs', ['AVC'])
return {
'supports_4k': max_res >= 2160,
'supports_hevc': 'HEVC' in codecs
}
return {'supports_4k': False, 'supports_hevc': False}
def supports_doorbell_press(device_capabilities):
button_press = device_capabilities.get('attributes', {}).get('button_press', {})
return button_press.get('supported') is True
Device status provides real-time information about device availability and connectivity. This is essential for determining whether devices can respond to requests or stream video.
Status information can be retrieved in two ways:
include parameter:
GET https://api.amazonvision.com/v1/devices?include=status
Authorization: Bearer <access_token>
GET https://api.amazonvision.com/v1/devices/{device_id}/status
Authorization: Bearer <access_token>
{
"meta": {
"time": "2025-07-07T10:30:00Z"
},
"data": {
"type": "device-status",
"id": "xxxyyy.status",
"attributes": {
"online": true
}
}
}
def is_device_available(device_status):
return device_status.get('attributes', {}).get('online', False)
def can_stream_video(device_id, device_status):
if is_device_available(device_status):
return True
else:
print(f"Device {device_id} is offline - streaming not available")
return False
When devices are offline:
Device location information provides coarse, non-specific geographic data about where Ring devices are installed. This information may be required for determining local legal compliance or regional feature availability.
Location information can be retrieved in two ways:
include parameter:
GET https://api.amazonvision.com/v1/devices?include=location
Authorization: Bearer <access_token>
GET https://api.amazonvision.com/v1/devices/{device_id}/location
Authorization: Bearer <access_token>
{
"meta": {
"time": "2025-07-07T10:30:00Z"
},
"data": {
"type": "locations",
"id": "zzzzzz",
"attributes": {
"country": "US",
"state": "GA"
}
}
}
Ring provides intentionally coarse location information to protect user privacy:
def check_regional_compliance(device_location):
country = device_location.get('attributes', {}).get('country')
state = device_location.get('attributes', {}).get('state')
if country == 'US':
if state in ['CA', 'IL']: # States with specific privacy laws
return apply_enhanced_privacy_controls()
elif country in ['GB', 'DE', 'FR']:
return apply_gdpr_controls()
return apply_default_controls()
Device configurations provide details about user-specified settings that may affect partner processing of video content and device behavior. These settings are particularly important for motion detection and privacy considerations.
Configuration information can be retrieved in two ways:
include parameter:
GET https://api.amazonvision.com/v1/devices?include=configurations
Authorization: Bearer <access_token>
GET https://api.amazonvision.com/v1/devices/{device_id}/configurations
Authorization: Bearer <access_token>
component_id query parameter:
GET https://api.amazonvision.com/v1/devices/{device_id}/configurations?component_id=5
Authorization: Bearer <access_token>
{
"meta": {
"time": "2026-02-14T23:37:48Z"
},
"data": {
"type": "device-configurations",
"id": "ava1.ring.device.configurations.XXXYYY",
"attributes": {
"e2e_encryption": {
"enabled": false
},
"motion_detection": {
"enabled": "on",
"motion_zones": [
{
"id": "a9d225a6-3122-4f4d-a933-0c24e53cc535",
"vertices": [
{"x": 0, "y": 0.2},
{"x": 0.5, "y": 0.2},
{"x": 1, "y": 0.2},
{"x": 1, "y": 0.6},
{"x": 1, "y": 1},
{"x": 0.5, "y": 1},
{"x": 0, "y": 1},
{"x": 0, "y": 0.6}
]
}
],
"component_id": "0"
},
"video": {
"component_id": "0"
},
"image_enhancements": {
"color_night_vision": "off",
"hdr": "off",
"ir_led_night_vision": "off",
"auto_zoom_track": "off",
"privacy_zones": []
}
}
}
}
id and vertices). When no privacy zones are configured, the array is empty ([]).true or false). When true, video requests for the device still succeed but return encrypted content your app cannot reade2e_encryption.enabled is a JSON boolean, not one of the on/off strings the rest of this response uses — motion_detection.enabled in the same payload is the string on. Compare e2e_encryption.enabled to true, not to a string. Read it before you attempt any video processing for a device, and treat unreadable media as an expected outcome for a subset of devices rather than retrying or surfacing an error. See Video encryption (TAKE and E2EE)."on" or "off")id and vertices arraymotion_detection.component_id — not to the device as a whole. Don't apply one module's zones to every view. See Multi-Camera Devices."on" or "off")"on" or "off")"on" or "off")"on" or "off")Scope the configurations request to one camera module with the component_id query parameter.
GET https://api.amazonvision.com/v1/devices/{device_id}/configurations?component_id=5
Authorization: Bearer <access_token>
{
"data": {
"type": "component-configurations",
"id": "ava1.ring.component.configurations.XXXYYY",
"attributes": {
"component_name": "View 6",
"component_id": "5",
"component_type": "lens"
}
},
"meta": {
"time": "2026-08-14T13:42:16Z"
}
}
The scoped response returns identity metadata only. Motion zones, video settings, and image enhancements aren't yet scoped per component — read them from the device-level configurations response, which describes the first component in components.items and names it in motion_detection.component_id.
Zones use a normalized coordinate system where all values are in the range 0.0 to 1.0:
0.0 = left edge, 1.0 = right edge0.0 = top edge, 1.0 = bottom edgeTo convert to pixel coordinates, multiply by the frame dimensions:
pixel_x = vertex['x'] * frame_width
pixel_y = vertex['y'] * frame_height
x and y values are JSON numbers. When a value falls on a whole number (e.g., 0, 1), JSON parsers may deserialize it as an Integer rather than a Double. In Java, do not cast vertex values directly to (Double) — use ((Number) value).doubleValue() instead. In Python, this is not an issue.def is_video_readable(device_config):
encryption = device_config.get('attributes', {}).get('e2e_encryption', {})
return not encryption.get('enabled', False)
def should_process_motion_events(device_config):
motion_config = device_config.get('attributes', {}).get('motion_detection', {})
return motion_config.get('enabled') == 'on'
def get_motion_zones(device_config):
motion_config = device_config.get('attributes', {}).get('motion_detection', {})
return motion_config.get('motion_zones', [])
def get_privacy_zones(device_config):
image_config = device_config.get('attributes', {}).get('image_enhancements', {})
return image_config.get('privacy_zones', [])
def should_obscure_regions(device_config):
privacy_zones = get_privacy_zones(device_config)
return len(privacy_zones) > 0
def point_in_polygon(point, vertices):
"""Check if a normalized point (0-1 range) is inside a polygon."""
x, y = point
n = len(vertices)
inside = False
p1x, p1y = vertices[0]['x'], vertices[0]['y']
for i in range(1, n + 1):
p2x, p2y = vertices[i % n]['x'], vertices[i % n]['y']
if y > min(p1y, p2y):
if y <= max(p1y, p2y):
if x <= max(p1x, p2x):
if p1y != p2y:
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
if p1x == p2x or x <= xinters:
inside = not inside
p1x, p1y = p2x, p2y
return inside
Number (not Double or Integer) to avoid type cast errorsSome Ring cameras contain more than one camera module in a single physical unit. Ring Elite cameras combine several camera modules covering different parts of a panoramic field of view, and report themselves to the API as one device.
Each camera module is a component, addressed by a component_id. Discovery tells you how many components a device has; a component_id query parameter lets you target one of them for capabilities, configurations, and live streaming.
| Model | Camera modules |
|---|---|
| Ring Elite 140 | Two |
| Ring Elite 180 | Three |
| Ring Elite 270 | Five |
| Ring Elite 360 | Six |
attributes.name is a name the customer chose. Use this table to know what to expect across the lineup, but always read the actual camera count from components.items at runtime.A multi-camera device exposes a components object in its device-capabilities resource. Single-camera devices omit components entirely — its absence is how you identify a standard single-camera device.
GET https://api.amazonvision.com/v1/devices?include=capabilities
Authorization: Bearer <access_token>
The components.items array lists every camera module in physical order. The following response is from a Ring Elite 360.
{
"type": "device-capabilities",
"id": "ava1.ring.device.capabilities.XXXYYY",
"attributes": {
"components": {
"items": [
{ "component_id": "0", "component_type": "lens", "component_name": "View 1" },
{ "component_id": "1", "component_type": "lens", "component_name": "View 2" },
{ "component_id": "2", "component_type": "lens", "component_name": "View 3" },
{ "component_id": "3", "component_type": "lens", "component_name": "View 4" },
{ "component_id": "4", "component_type": "lens", "component_name": "View 5" },
{ "component_id": "5", "component_type": "lens", "component_name": "View 6" }
]
}
}
}
| Field | Type | Description |
|---|---|---|
component_id |
string | Identifier for one camera module. Pass this as the component_id query parameter. |
component_type |
string | Module type. "lens" for a camera module. Treat unrecognized values as unsupported and skip them. |
component_name |
string | Display label for the module, such as "View 1". Show this to users instead of the raw component_id. |
component_id is a string and opaque. Values on current devices look like "0" through "5", but don't parse them as integers, sort them numerically, or generate them yourself. Read them from components.items and pass them back unchanged.components.items is ordered to match the physical arrangement of the camera modules on the device, and that order is stable across API calls. Use the array order — not the component_id value — when you lay out views side by side or build a panoramic composite.
Ring doesn't expose a pre-stitched panoramic stream through the partner API. To present a panorama, stream each component and compose the views in your own application using the array order.
Two mechanisms, depending on the endpoint.
Query parameter — metadata and live streaming:
| Endpoint | Purpose |
|---|---|
GET /v1/devices/{device_id}/capabilities?component_id=N |
Metadata for one camera module |
GET /v1/devices/{device_id}/configurations?component_id=N |
Settings scoped to one camera module |
POST /v1/devices/{device_id}/media/streaming/whep/sessions?component_id=N |
Live WHEP stream from one camera module |
rtsps://video.rtsp.amazonvision.com:322/v1/devices/{device_id}/stream?component_id=N |
Live RTSP stream from one camera module |
Request body — media downloads. These take a components array instead of a query parameter:
| Endpoint | Purpose |
|---|---|
POST /v1/devices/{device_id}/media/video/download |
Recorded clip from one camera module |
POST /v1/devices/{device_id}/media/image/download |
Snapshot from one camera module |
"components": [
{ "component_id": "1" }
]
The array takes exactly one entry — one request per camera module. See Media Clips and Image Snapshots.
component_idFor capabilities, configurations, and live streaming, omitting component_id targets the first component in components.items. Device-level capabilities and configurations responses describe that first component, and carry component_id inside the motion_detection and video blocks so you can confirm which one you're reading.
An application with no multi-camera support keeps working against a Ring Elite: it discovers one device, streams one view, and receives device-level webhook events. Single-camera devices ignore component_id entirely.
components in the request body. On a multi-camera device, the video and image download endpoints do not currently guarantee which camera module you get when components is omitted. Send it on every download request against a Ring Elite. See Media Clips and Image Snapshots.Motion webhooks fire at the device level. On a multi-camera device the payload carries a component_ids array naming the components that contributed to the event. The field is absent on single-camera devices.
{
"data": {
"type": "motion_detected",
"attributes": {
"source": "ava1.ring.device.XXXYYY",
"source_type": "devices",
"timestamp": 1786715596787,
"sub_type": "motion",
"component_ids": ["0"]
}
}
}
See Motion Detection for the full payload.
The following code example discovers components and starts a stream for each one. It works unchanged across the Ring Elite lineup and on single-camera devices.
def get_components(device_capabilities):
"""Return component IDs for a device, or a single default entry."""
components = device_capabilities.get("attributes", {}).get("components")
if not components:
return [None] # Single-camera device — omit component_id
return [c["component_id"] for c in components["items"]]
def stream_all_views(device_id, device_capabilities, access_token, sdp_offer):
base = f"https://api.amazonvision.com/v1/devices/{device_id}/media/streaming/whep/sessions"
sessions = []
for component_id in get_components(device_capabilities):
url = base if component_id is None else f"{base}?component_id={component_id}"
sessions.append(start_whep_session(url, access_token, sdp_offer))
return sessions
Because components takes a single entry, downloading every view means one request per module.
def download_all_views(device_id, device_capabilities, access_token, timestamp):
"""One request per camera module — components takes exactly one entry."""
results = {}
for component_id in get_components(device_capabilities):
payload = {"type": "at_timestamp", "timestamp": timestamp}
if component_id is not None:
payload["components"] = [{"component_id": component_id}]
results[component_id] = post_image_download(device_id, access_token, payload)
return results
component_idsRing provides real-time notifications to partners through webhook-based callbacks. This enables partners to receive timely updates about device events, changes, and user actions.
The notification system uses:
X-Signature header for message authenticitymeta for user associationBefore receiving webhooks, your endpoint must meet the following requirements:
| Requirement | Detail |
|---|---|
| Protocol | HTTPS only (TLS 1.2 or higher required) |
| Accessibility | Endpoint must be publicly accessible from Ring's servers |
| Registration | Webhook URL is configured during partner onboarding |
| Response time | Must respond within 5 seconds or Ring treats the delivery as failed |
| Content-Type | Ring sends application/json; your endpoint must accept this content type |
| Method | Ring sends POST requests exclusively |
All webhook requests from Ring include an HMAC-SHA256 signature in the X-Signature header. Partners must verify this signature using the HMAC signing key issued with their partner credentials before processing any webhook payload:
POST https://api.partner.example.com/webhooks/ring
X-Signature: sha256=<hmac_signature>
Content-Type: application/json
import hmac
import hashlib
def verify_webhook_signature(signing_key, raw_body, received_signature):
"""Verify the HMAC-SHA256 signature of a Ring webhook."""
expected = hmac.new(
signing_key.encode(),
raw_body,
hashlib.sha256
).hexdigest()
received = received_signature.removeprefix("sha256=")
# Constant-time comparison to prevent timing attacks
return hmac.compare_digest(expected, received)
raw_body must be the request body exactly as received (in Flask, request.data). If you parse the JSON and then re-serialize it to compute the HMAC, whitespace and key ordering change the bytes — the digest will not match and every signature check will fail with a payload that looks correct. Verify first, parse second..hexdigest()), while Nonces use URL-safe Base64 without padding. Do not mix encodings.All Ring webhooks follow the v1.1 payload format with account_id in the meta section:
{
"meta": {
"version": "1.1",
"time": "2026-02-13T13:39:57.200155525Z",
"request_id": "2ad45ade-1818-4154-813b-afdd8bcd8085",
"account_id": "ava1.ring.account.XXXYYY"
},
"data": {
"id": "<device_id>_<event_type>_<timestamp>",
"type": "<event_type>",
"attributes": {
"source": "<device_id>",
"source_type": "devices",
"timestamp": 1770989995231
},
"relationships": {
"devices": {
"links": {
"self": "/v1/devices/<device_id>"
}
}
}
}
}
1.1 for the current webhook specificationcomponent_ids array inside attributes, naming the camera modules that contributed to the event. Events still fire at the device level. See Multi-Camera Devices.Ring supports the following webhook event types:
| Event | Type Value | Description | Details |
|---|---|---|---|
| Motion Detected | motion_detected |
Motion detected by a device camera. Includes attributes.sub_type (such as human), plus attributes.component_ids on multi-camera devices |
Motion Detection |
| Button Press | button_press |
Ring doorbell button pressed | Button Press |
| Device Added | device_added |
A device became accessible to the partner | Device Addition |
| Device Removed | device_removed |
A device is no longer accessible | Device Removal |
| Device Online | device_online |
A device came online | Device Online |
| Device Offline | device_offline |
A device went offline | Device Offline |
| App Integration Added | app_integration_added |
A Ring user linked a partner integration | App Integration Added |
| App Integration Removed | app_integration_removed |
A Ring user unlinked a partner integration | App Integration Removed |
Subscription lifecycle webhooks follow the v1.1 payload format with account_id in the meta section:
{
"meta": {
"version": "1.1",
"time": "2026-07-06T11:11:00Z",
"request_id": "3c4484f3-4605-4bad-920d-4583cd2bef69",
"account_id": "ava1.ring.account.XXXYYY"
},
"data": {
"id": "<event_id>",
"type": "subscription_activated",
"attributes": {
"source": "subscriptions",
"source_id": "<subscription_id>",
"sub_type": "paid",
"plan_id": "<plan_id>",
"expires_at": "2026-08-06T00:00:00Z"
},
"relationships": {
"devices": {
"data": [
{ "type": "device", "id": "<device_id>" }
]
},
"subscriptions": {
"links": {
"self": "/v1/accounts/me/subscriptions"
}
}
}
}
}
1.1 for the current webhook specificationRing supports the following subscription lifecycle event types:
| Event | Type Value | Description | Details |
|---|---|---|---|
| Subscription Activated | subscription_activated |
A paid subscription or trial was activated | Subscription Activated |
| Subscription Deactivated | subscription_deactivated |
A paid subscription or trial was deactivated | Subscription Deactivated |
Your endpoint must respond with an appropriate HTTP status code:
| Status Code | Ring Behavior | Details |
|---|---|---|
| 200-299 | Delivery successful | Ring marks the event as delivered. No retry. |
| 400-499 | Permanent failure — no retry | Indicates a client-side issue. Ring will not retry these. |
| 500-599 | Temporary failure — retry with backoff | Ring will retry delivery using exponential backoff. |
| Timeout | Treated as 5xx — retry with backoff | If your endpoint does not respond within 5 seconds, Ring treats it as a server error. |
Ring implements automatic retry logic for failed webhook deliveries (5xx responses and timeouts):
| Attempt | Delay After Previous Attempt |
|---|---|
| 1 (initial) | Immediate |
| 2 | 1 second |
| 3 | 5 seconds |
| 4 | 30 seconds |
| 5 | 2 minutes |
| 6 | 10 minutes |
| 7 | 1 hour |
meta.request_id field to deduplicate events.from flask import Flask, request, jsonify
import hmac
import hashlib
app = Flask(__name__)
SIGNING_KEY = "your_hmac_signing_key"
@app.route('/webhook/ring', methods=['POST'])
def handle_ring_webhook():
# Verify HMAC signature
signature = request.headers.get('X-Signature', '')
if not verify_webhook_signature(SIGNING_KEY, request.data, signature):
return jsonify({'error': 'Invalid signature'}), 401
# Parse payload
try:
payload = request.get_json()
event_type = payload['data']['type']
account_id = payload['meta']['account_id']
# Route to appropriate handler
if event_type == 'motion_detected':
handle_motion_detection(payload)
elif event_type == 'button_press':
handle_button_press(payload)
elif event_type == 'device_added':
handle_device_addition(payload)
elif event_type == 'device_removed':
handle_device_removal(payload)
elif event_type == 'device_online':
handle_device_online(payload)
elif event_type == 'device_offline':
handle_device_offline(payload)
elif event_type == 'app_integration_added':
handle_app_integration_added(payload)
elif event_type == 'app_integration_removed':
handle_app_integration_removed(payload)
elif event_type == 'subscription_activated':
handle_subscription_activated(payload)
elif event_type == 'subscription_deactivated':
handle_subscription_deactivated(payload)
else:
return jsonify({'error': 'Unknown event type'}), 400
return jsonify({'status': 'processed'}), 200
except Exception as e:
log_error(f"Webhook processing failed: {e}")
return jsonify({'error': 'Processing failed'}), 500
def handle_webhook_with_idempotency(payload):
request_id = payload['meta']['request_id']
if is_already_processed(request_id):
return {'status': 'already_processed'}
result = process_webhook(payload)
mark_as_processed(request_id)
return result
| Mistake | Consequence | Fix |
|---|---|---|
| Not verifying HMAC signatures | Accepts spoofed webhooks | Always call verify_webhook_signature() before processing |
| Signing re-serialized JSON | Every signature check fails with 401, even though the payload looks correct |
Compute the HMAC over the raw body bytes as received (request.data), before parsing |
| Doing heavy processing in handler | Exceeds 5-second timeout, causes duplicates | Return 200 immediately, process asynchronously |
| Not handling duplicate events | Business logic executes multiple times | Use meta.request_id as idempotency key |
| Returning 4xx for transient errors | Ring will not retry | Return 500 for recoverable errors |
| Ignoring dead letter queue | Missed events during outages | Monitor and request replays within 72-hour window |
X-Signature headermeta.account_idrequest_idMotion detection notifications are sent when Ring devices detect movement in their field of view. These real-time notifications enable partners to respond immediately to motion events.
Ring delivers all webhook notifications with an HMAC-SHA256 signature in the X-Signature header. Partners must verify this signature before processing the payload. See Notifications for signature verification details.
{
"meta": {
"version": "1.1",
"time": "2026-02-13T13:39:57.200155525Z",
"request_id": "2ad45ade-1818-4154-813b-afdd8bcd8085",
"account_id": "ava1.ring.account.XXXYYY"
},
"data": {
"id": "<device_id>_motion_<timestamp>",
"type": "motion_detected",
"attributes": {
"source": "<device_id>",
"source_type": "devices",
"timestamp": 1786715596787,
"timestamp_readable": "2026-08-14 08:53:16",
"sub_type": "motion",
"component_ids": ["0"]
},
"relationships": {
"devices": {
"links": {
"self": "/v1/devices/<device_id>"
}
}
}
}
}
1.1 includes account_id)<device_id>_<sub_type>_<timestamp>)motion_detected for motion eventsmotion, human). Returned inside attributes.devices for device-originated eventstimestamp, for logs and debugging. No timezone offset is included — use timestamp for any time calculation.The values sub_type can carry depend on whether the device has Smart Alerts enabled. Smart Alerts is the Ring-side detection that classifies what triggered a motion event; customers turn it on per device in the Ring app.
Smart Alerts disabled. No classification is performed, and every motion event from the device carries one value:
| Sub Type | Description |
|---|---|
| motion | General motion detected |
Smart Alerts enabled. Ring classifies the trigger, and sub_type carries one of:
| Sub Type | Description |
|---|---|
| human | Human detected |
| vehicle | Vehicle detected |
| package_delivery | Package delivery detected |
| other_motion | Unable to categorize motion |
sub_type is returned inside attributes — read it as data.attributes.sub_type, not data.subType.sub_type. Treat the field as an open-ended vocabulary: handle the values you act on, and pass anything else through rather than dropping it. See Refer to Ring app for device configuration settings.def handle_motion_detection(payload):
attributes = payload['data']['attributes']
device_id = attributes['source']
account_id = payload['meta']['account_id']
motion_timestamp = attributes['timestamp']
event_id = payload['data']['id']
sub_type = attributes.get('sub_type', 'unknown')
# Multi-camera devices name the modules that triggered the event
component_ids = attributes.get('component_ids', [])
log_motion_event(device_id, account_id, motion_timestamp, event_id, sub_type, component_ids)
trigger_motion_response(device_id, account_id, motion_timestamp, sub_type, component_ids)
return {'status': 'processed'}
X-Signature header before processingmeta.account_idattributes.sub_type (e.g., human) to differentiate motion classificationsattributes.component_ids to attribute the event to specific camera modulesrequest_id or event idButton press notifications are sent when a Ring doorbell button is pressed. These real-time notifications enable partners to respond immediately to doorbell press events.
Ring delivers all webhook notifications with an HMAC-SHA256 signature in the X-Signature header. Partners must verify this signature before processing the payload. See Notifications for signature verification details.
{
"meta": {
"version": "1.1",
"time": "2026-02-14T00:02:53.027052438Z",
"request_id": "c83081fc-2f1b-4cb7-8b18-b18a318b8bab",
"account_id": "ava1.ring.account.XXXYYY"
},
"data": {
"id": "<device_id>_button_press_<timestamp>",
"type": "button_press",
"attributes": {
"source": "<device_id>",
"source_type": "devices",
"timestamp": 1771027372393
},
"relationships": {
"devices": {
"links": {
"self": "/v1/devices/<device_id>"
}
}
}
}
}
1.1 includes account_id)<device_id>_button_press_<timestamp>)button_press for doorbell press eventsdevicesButton press notifications are triggered when:
You can tell whether a device sends button press events before any event arrives. Its capabilities return button_press.supported: true on a doorbell, and no button_press object on other devices. Read it during discovery with GET /v1/devices?include=capabilities, and use it to decide which experience to offer for each device.
def handle_button_press(payload):
device_id = payload['data']['attributes']['source']
account_id = payload['meta']['account_id']
press_timestamp = payload['data']['attributes']['timestamp']
event_id = payload['data']['id']
log_button_press(device_id, account_id, press_timestamp, event_id)
trigger_doorbell_response(device_id, account_id, press_timestamp)
return {'status': 'processed'}
X-Signature header before processingmeta.account_idrequest_id or event idDevice addition notifications are sent when a new Ring device becomes available for partner access. This occurs when a user links your app, adds devices to their account, or grants additional permissions to the partner integration.
Ring delivers all webhook notifications with an HMAC-SHA256 signature in the X-Signature header. Partners must verify this signature before processing the payload. See Notifications for signature verification details.
{
"meta": {
"version": "1.1",
"time": "2026-02-13T12:20:20.935994571Z",
"request_id": "216c97be-4d57-45c1-a9f0-28c9f5442e8c",
"account_id": "ava1.ring.account.XXXYYY"
},
"data": {
"id": "<device_id>_device_added_<timestamp>",
"type": "device_added",
"attributes": {
"source": "<device_id>",
"source_type": "devices",
"timestamp": 1770985219558
},
"relationships": {
"devices": {
"links": {
"self": "/v1/devices/<device_id>"
}
}
}
}
}
Device addition notifications are triggered when:
/v1/accounts/me/app-integrations during one-way account linking: one event for each device the user shareddevice_added events from linking arrive within seconds of your POST, while the integration is still awaiting, and can arrive before the POST call returns. Process them normally, and still make sure your PATCH to completed succeeds. Delivery is at-least-once, so after the PATCH succeeds, call GET /v1/devices for the initial device list, and use device_added and device_removed events to track changes from then on.def handle_device_addition(payload):
device_id = payload['data']['attributes']['source']
account_id = payload['meta']['account_id']
# Fetch full device information
device_info = fetch_device_details(device_id)
if device_info:
store_new_device(device_info, account_id)
setup_device_monitoring(device_id)
return {'status': 'device_added'}
else:
return {'status': 'error', 'message': 'Failed to fetch device details'}
X-Signature header before processingmeta.account_idGET /v1/devices/{id}?include=capabilities,status after receiving the notificationrequest_id or event idDevice removal notifications are sent when a partner loses access to a Ring device. This occurs when users revoke permissions, remove devices, or modify their integration settings.
Ring delivers all webhook notifications with an HMAC-SHA256 signature in the X-Signature header. Partners must verify this signature before processing the payload. See Notifications for signature verification details.
{
"meta": {
"version": "1.1",
"time": "2026-02-13T19:28:04.020279817Z",
"request_id": "668b822c-266b-43c4-a1f0-6045bc5f3b1c",
"account_id": "ava1.ring.account.XXXYYY"
},
"data": {
"id": "<device_id>_device_removed_<timestamp>",
"type": "device_removed",
"attributes": {
"source": "<device_id>",
"source_type": "devices",
"timestamp": 1771010883164
},
"relationships": {
"devices": {
"links": {
"self": "/v1/devices/<device_id>"
}
}
}
}
}
Device removal notifications are triggered when:
def handle_device_removal(payload):
device_id = payload['data']['attributes']['source']
account_id = payload['meta']['account_id']
removal_timestamp = payload['data']['attributes']['timestamp']
# Stop active streaming sessions
terminate_active_streams(device_id)
# Cancel scheduled operations
cancel_scheduled_operations(device_id)
# Clean up stored data
cleanup_device_resources(device_id)
# Update device status in partner system
mark_device_removed(device_id, account_id, removal_timestamp)
return {'status': 'device_removed'}
After device removal:
X-Signature header before processingmeta.account_idDevice online notifications are sent when a Ring device comes online and becomes available for interaction. These real-time notifications enable partners to know when devices are reachable for streaming, configuration, or other operations.
Ring delivers all webhook notifications with an HMAC-SHA256 signature in the X-Signature header. Partners must verify this signature before processing the payload. See Notifications for signature verification details.
{
"meta": {
"version": "1.1",
"time": "2026-02-13T23:32:51.234231900Z",
"request_id": "ab058f6f-8edd-4e92-9796-956ddc4d739d",
"account_id": "ava1.ring.account.XXXYYY"
},
"data": {
"id": "<device_id>_device_online_<timestamp>",
"type": "device_online",
"attributes": {
"source": "<device_id>",
"source_type": "devices",
"timestamp": 1771025567000
},
"relationships": {
"devices": {
"links": {
"self": "/v1/devices/<device_id>"
}
}
}
}
}
Device online notifications are triggered when:
def handle_device_online(payload):
device_id = payload['data']['attributes']['source']
account_id = payload['meta']['account_id']
online_timestamp = payload['data']['attributes']['timestamp']
update_device_status(device_id, account_id, 'online', online_timestamp)
resume_device_operations(device_id)
return {'status': 'processed'}
X-Signature header before processingmeta.account_idrequest_id or event idDevice offline notifications are sent when a Ring device goes offline and is no longer reachable. These real-time notifications enable partners to handle device unavailability gracefully and inform users.
Ring delivers all webhook notifications with an HMAC-SHA256 signature in the X-Signature header. Partners must verify this signature before processing the payload. See Notifications for signature verification details.
{
"meta": {
"version": "1.1",
"time": "2026-02-13T23:26:49.710448203Z",
"request_id": "343385ae-1de8-4b29-85aa-07bd4b7112fd",
"account_id": "ava1.ring.account.XXXYYY"
},
"data": {
"id": "<device_id>_device_offline_<timestamp>",
"type": "device_offline",
"attributes": {
"source": "<device_id>",
"source_type": "devices",
"timestamp": 1771025206000
},
"relationships": {
"devices": {
"links": {
"self": "/v1/devices/<device_id>"
}
}
}
}
}
Device offline notifications are triggered when:
def handle_device_offline(payload):
device_id = payload['data']['attributes']['source']
account_id = payload['meta']['account_id']
offline_timestamp = payload['data']['attributes']['timestamp']
update_device_status(device_id, account_id, 'offline', offline_timestamp)
terminate_active_streams(device_id)
pause_device_operations(device_id)
return {'status': 'processed'}
X-Signature header before processingmeta.account_idrequest_id or event idApp integration added notifications are sent when a Ring user links your partner integration to their Ring account. In one-way account linking, Ring sends this event when it verifies the nonce in your POST to /v1/accounts/me/app-integrations. It arrives while the integration is still awaiting, and can arrive before the POST call returns. The link isn't complete until your PATCH with status: completed succeeds.
Ring delivers all webhook notifications with an HMAC-SHA256 signature in the X-Signature header. Partners must verify this signature before processing the payload. See Notifications for signature verification details.
{
"meta": {
"version": "1.1",
"time": "2026-07-06T11:11:00Z",
"request_id": "3c4484f3-4605-4bad-920d-4583cd2bef69",
"account_id": "ava1.ring.account.XXXYYY"
},
"data": {
"id": "<event_id>",
"type": "app_integration_added",
"attributes": {
"source": "<account_id>",
"source_type": "users",
"timestamp": 1699457230000
}
}
}
Note: The
relationshipsblock is not included in this event. After integration addition, the partner's access to the user's devices is established through the account linking flow.
1.1 includes account_id)app_integration_added for this event"users" — integration events are user-initiated actionsThis event is sent when:
/v1/accounts/me/app-integrations during one-way account linkingdef handle_app_integration_added(payload):
account_id = payload['meta']['account_id']
request_id = payload['meta']['request_id']
event_timestamp = payload['data']['attributes']['timestamp']
# Deduplicate using request_id
if is_duplicate_event(request_id):
return {'status': 'duplicate'}
mark_event_processed(request_id)
# Initialize the user's integration in your system
initialize_integration(account_id, event_timestamp)
return {'status': 'processed'}
X-Signature header before processingmeta.account_idrequest_idApp integration removed notifications are sent when a Ring user unlinks or removes your partner integration from their Ring account. This event signals that the partner's access to the user's account, devices, and data has been revoked.
Ring delivers all webhook notifications with an HMAC-SHA256 signature in the X-Signature header. Partners must verify this signature before processing the payload. See Notifications for signature verification details.
{
"meta": {
"version": "1.1",
"time": "2026-07-06T11:11:00Z",
"request_id": "3c4484f3-4605-4bad-920d-4583cd2bef69",
"account_id": "ava1.ring.account.XXXYYY"
},
"data": {
"id": "<event_id>",
"type": "app_integration_removed",
"attributes": {
"source": "<account_id>",
"source_type": "users",
"timestamp": 1699457230000
}
}
}
Note: The
relationshipsblock is not included in this event. After integration removal, the partner's AVA access and refresh tokens are revoked — any API calls using those tokens fail with 401 Unauthorized.
1.1 includes account_id)app_integration_removed for this event"users" — integration events are user-initiated actionsThis event is sent when:
def handle_app_integration_removed(payload):
account_id = payload['meta']['account_id']
request_id = payload['meta']['request_id']
event_timestamp = payload['data']['attributes']['timestamp']
# Deduplicate using request_id
if is_duplicate_event(request_id):
return {'status': 'duplicate'}
mark_event_processed(request_id)
# Clean up the user's integration
terminate_active_sessions(account_id)
cancel_scheduled_operations(account_id)
remove_cached_tokens(account_id)
mark_integration_removed(account_id, event_timestamp)
return {'status': 'processed'}
X-Signature header before processingmeta.account_idrequest_idSubscription activated notifications are sent when a user's paid subscription or trial is activated for your app. This event signals that you should grant access to your services for the associated devices.
Ring delivers all webhook notifications with an HMAC-SHA256 signature in the X-Signature header. Partners must verify this signature before processing the payload. See Notifications for signature verification details.
{
"meta": {
"version": "1.1",
"time": "2026-07-06T11:11:00Z",
"request_id": "3c4484f3-4605-4bad-920d-4583cd2bef69",
"account_id": "ava1.ring.account.XXXYYY"
},
"data": {
"id": "<event_id>",
"type": "subscription_activated",
"attributes": {
"source": "subscriptions",
"source_id": "<subscription_id>",
"sub_type": "paid",
"plan_id": "<plan_id>",
"expires_at": "2026-08-06T00:00:00Z"
},
"relationships": {
"devices": {
"data": [
{ "type": "device", "id": "<device_id>" }
]
},
"subscriptions": {
"links": {
"self": "/v1/accounts/me/subscriptions"
}
}
}
}
}
1.1 includes account_id)subscription_activated for this event"subscriptions" for subscription events"paid" — Paid subscription (purchase, renewal, or reactivation)"trial" — Free trial enrollment or trial resumednull for trials.subscription_activated."device" (singular).This event is sent when:
The webhook payload notifies you that a subscription state change occurred. For the most reliable integration, use the webhook as a trigger and then call the Subscriptions API to retrieve the full, current subscription state:
import requests
def handle_subscription_activated(payload, ava_token):
account_id = payload['meta']['account_id']
request_id = payload['meta']['request_id']
# Deduplicate using request_id
if is_duplicate_event(request_id):
return {'status': 'duplicate'}
mark_event_processed(request_id)
# Fetch full subscription state from the Subscriptions API
response = requests.get(
"https://api.amazonvision.com/v1/accounts/me/subscriptions",
headers={"Authorization": f"Bearer {ava_token}"}
)
if response.status_code == 200:
subscriptions = response.json().get('data', [])
for sub in subscriptions:
sub_type = sub['attributes']['sub_type']
status = sub['attributes']['status']
plan_id = sub['attributes'].get('plan_id')
expires_at = sub['attributes']['expires_at']
devices = [d['id'] for d in sub['relationships']['devices']['data']]
if status == 'active':
grant_access(account_id, devices, sub_type, plan_id, expires_at)
return {'status': 'processed'}
This approach ensures you always have the latest subscription state, even if webhooks arrive out of order or are duplicated.
X-Signature header before processingmeta.account_id"paid" and "trial" activationsrequest_idSubscription deactivated notifications are sent when a user's paid subscription or trial is deactivated for your app. This event signals that you should revoke access immediately for the associated devices.
Ring delivers all webhook notifications with an HMAC-SHA256 signature in the X-Signature header. Partners must verify this signature before processing the payload. See Notifications for signature verification details.
{
"meta": {
"version": "1.1",
"time": "2026-07-06T11:11:00Z",
"request_id": "3c4484f3-4605-4bad-920d-4583cd2bef69",
"account_id": "ava1.ring.account.XXXYYY"
},
"data": {
"id": "<event_id>",
"type": "subscription_deactivated",
"attributes": {
"source": "subscriptions",
"source_id": "<subscription_id>",
"sub_type": "paid",
"plan_id": "<plan_id>"
},
"relationships": {
"devices": {
"data": [
{ "type": "device", "id": "<device_id>" }
]
},
"subscriptions": {
"links": {
"self": "/v1/accounts/me/subscriptions"
}
}
}
}
}
expires_at field is not present in deactivation events. Revoke access immediately upon receiving this event.1.1 includes account_id)subscription_deactivated for this event"subscriptions" for subscription events"paid" — Paid subscription deactivated"trial" — Free trial cancelled or suspendednull for trials."device" (singular).| Field | subscription_activated | subscription_deactivated |
|---|---|---|
data.type |
"subscription_activated" |
"subscription_deactivated" |
attributes.expires_at |
Present (ISO 8601) | Not present |
This event is sent when:
Use the webhook as a trigger and call the Subscriptions API to confirm the current subscription state before revoking access:
import requests
def handle_subscription_deactivated(payload, ava_token):
account_id = payload['meta']['account_id']
request_id = payload['meta']['request_id']
# Deduplicate using request_id
if is_duplicate_event(request_id):
return {'status': 'duplicate'}
mark_event_processed(request_id)
# Fetch current subscription state from the Subscriptions API
response = requests.get(
"https://api.amazonvision.com/v1/accounts/me/subscriptions",
headers={"Authorization": f"Bearer {ava_token}"}
)
if response.status_code == 200:
subscriptions = response.json().get('data', [])
active_device_ids = set()
for sub in subscriptions:
if sub['attributes']['status'] == 'active':
for d in sub['relationships']['devices']['data']:
active_device_ids.add(d['id'])
# Revoke access for devices no longer covered by any active subscription
deactivated_devices = [
d['id'] for d in payload['data']['relationships']['devices']['data']
]
for device_id in deactivated_devices:
if device_id not in active_device_ids:
revoke_access(account_id, device_id)
return {'status': 'processed'}
This approach prevents accidentally revoking access if the user has another active subscription covering the same device.
X-Signature header before processingmeta.account_id"paid" and "trial" deactivationsrequest_idRing sensors report physical conditions: a door opening, water reaching a floor, a temperature crossing a threshold. A sensor is a devices resource, so the device endpoints you already use for cameras and doorbells work unchanged. What differs is that a sensor has no camera and no media endpoints.
This section covers all Ring sensors. Behavior is shared across devices unless a device subsection says otherwise.
The following table shows the sensors this section covers.
| Device | Reports | Events |
|---|---|---|
| Contact sensors, indoor and outdoor | Door and window open and close, tamper | contact_sensor_faulted, contact_sensor_cleared, tamper_detected, tamper_cleared |
| Flood/freeze sensor | Water, freezing, tamper | flood_detected, flood_cleared, freeze_detected, freeze_cleared, tamper_detected, tamper_cleared |
| Indoor temperature/humidity sensor | Temperature and humidity thresholds | temperature_exceeded, temperature_cleared, humidity_exceeded, humidity_cleared |
| Indoor air quality monitor | PM2.5, carbon monoxide, temperature, and humidity thresholds | pm25_exceeded, pm25_cleared, co_exceeded, co_cleared, temperature_exceeded, temperature_cleared, humidity_exceeded, humidity_cleared |
Every sensor also emits device_online and device_offline. See Connectivity events for sensors.
Ring cameras, doorbells, and chimes are not sensors for the purposes of this documentation. Their events are covered by Motion Detection, Button Press, and the other camera event sections.
Sensors are returned by the same call as your cameras and doorbells.
GET /v1/devices HTTP/1.1
Authorization: Bearer <access_token>
There is no sensor-specific discovery endpoint and no sensor-specific query endpoint.
| Endpoint | Notes |
|---|---|
GET /v1/devices |
Sensors appear in the same collection as cameras |
GET /v1/devices/{device_id} |
|
GET /v1/devices/{device_id}/status |
Attribute set varies by device. On devices that measure a value, includes the last reported reading. |
GET /v1/devices/{device_id}/capabilities |
Declares whether the device supports battery reporting. Read it before trusting battery_status. |
GET /v1/devices/{device_id}/location |
|
GET /v1/devices/{device_id}/configurations |
Returns the configured temperature and humidity thresholds on devices that have them. Remaining attributes are camera-oriented and null. |
?include=status,capabilities,location |
Mechanics unchanged |
GET /v1/devices returns sensors alongside your cameras, so code that iterates every device and calls a streaming or download endpoint will hit sensors.| Guidance | Detail |
|---|---|
| Do not infer device type from capabilities | Capability keys are shared across device families. Alarm motion sensors declare motion_detection, the same key a doorbell declares, so the presence of that key does not identify a camera device. |
| Store your own device type mapping at setup time | Sensors that share a signal share their attributes and event types. A temperature threshold event from an air quality monitor is indistinguishable from one sent by a temperature/humidity sensor. |
Every sensor returns these attributes from GET /v1/devices/{device_id}/status.
"attributes": {
"online": true,
"reported_at": "2026-03-10T14:20:05.118Z",
"signal_strength": {
"value": "good"
},
"battery_status": {
"percentage": 85
},
"audio": {
"snooze": {
"active": null,
"until": null
}
},
"state": null
}
| Attribute | Description |
|---|---|
online |
Whether Ring currently considers the device reachable. |
reported_at |
Last check-in time, as an ISO 8601 timestamp. Updated on every device uplink. |
signal_strength.value |
Signal quality. |
battery_status.percentage |
Integer percentage on a battery-powered device. Values pass through exactly as the device reports them. |
sensor_reporting_state.value |
Reads active even on a device that has never reported, so it does not indicate that the sensor is currently reporting. Read online and reported_at for liveness. |
online before reading any other attribute. A device that has not yet checked in returns placeholder values rather than an error, and the placeholder is not consistent: battery_status and signal_strength are absent entirely, while reported_at, online, and state return either null or, for reported_at, the Unix epoch 1970-01-01T00:00:00Z. Code that reads these keys directly raises on a newly added device, and a comparison against the epoch silently treats the device as having checked in decades ago.| Attribute | Contact | Flood/Freeze | Temp/Humidity | Air Quality |
|---|---|---|---|---|
contact_detection.faulted |
yes | — | — | — |
flood_detection.faulted |
— | yes | — | — |
freeze_detection.faulted |
— | yes | — | — |
tamper_detection.detected |
yes | yes | — | — |
sensor_reporting_state.value |
yes | yes | — | — |
capabilities before trusting battery_status.percentage. GET /v1/devices/{device_id}/capabilities returns battery_status.supported on a battery-powered device and battery_status: null on one that is not. A device that does not support battery reporting still returns a battery_status.percentage in status, but the value is outside the 0–100 range — the air quality monitor reports 255. Treat an out-of-range value as no battery rather than as a level.faulted: true means the condition is active. An open door reports contact_detection.faulted: true. The faulted naming is inherited from the underlying protocol and does not read intuitively. Tamper alone uses detected rather than faulted.The same contact sensor closed, then open.
"contact_detection": {
"faulted": false
}
"contact_detection": {
"faulted": true
}
A device that measures a value reports it in the status response, alongside the common telemetry.
| Device | Reading attributes |
|---|---|
| Indoor temperature/humidity sensor | temperature, humidity |
| Indoor air quality monitor | temperature, humidity, pm2_5, co |
Contact sensors and the flood/freeze sensor report detection attributes rather than readings.
Temperature is in degrees Celsius, humidity is a relative percentage, and PM2.5 is in µg/m³. All are floats.
"temperature": 24.2,
"humidity": 44.3,
"pm2_5": 3.5,
"co": 0.0
reported_at alongside it to judge how current it is. Observed readings have lagged the response by as much as an hour.pm2_5; the webhook payload calls it pm25_level. Temperature and humidity use the same name on both surfaces. Code that shares a parser across the two will miss the particulate reading on one of them.GET /v1/devices/{device_id}/configurations returns the temperature and humidity thresholds that drive threshold events, on devices that have them.
"temperature_threshold": {
"high": 32.2,
"low": 25.0
},
"humidity_threshold": {
"high": 85.0,
"low": 65.0
}
| Attribute | Description |
|---|---|
temperature_threshold.high, temperature_threshold.low |
Upper and lower temperature bounds, in degrees Celsius. |
humidity_threshold.high, humidity_threshold.low |
Upper and lower relative humidity bounds, as percentages. |
Crossing either bound emits the matching threshold event. A reading below low or above high produces an _exceeded event, and a return between them produces a _cleared event.
The response also carries motion_detection, image_enhancements, and audio. These are camera-oriented and null on every sensor.
Sensor state changes are delivered as webhooks. Ring signs every notification with an HMAC-SHA256 signature in the X-Signature header. Verify the signature before processing the payload, and return a 2xx response within 5 seconds. See Notifications.
Webhook delivery is the only source of sensor event data, which makes one field unusually important. Deduplicate on meta.request_id, the unique identifier Ring assigns to each delivery, because no event history exists to reconcile a duplicate against. Treat the value as opaque.
Each table below uses the same four columns. Every event uses the sensor webhook payload with data.type set to the type value shown.
Contact sensors.
| Event | Type value | Trigger | Payload attributes |
|---|---|---|---|
| Contact Sensor Faulted | contact_sensor_faulted |
The contact opens | — |
| Contact Sensor Cleared | contact_sensor_cleared |
The contact closes | — |
| Tamper Detected | tamper_detected |
The device is removed from its mount or physically altered | — |
| Tamper Cleared | tamper_cleared |
The tamper condition ends | — |
Flood/freeze sensor.
| Event | Type value | Trigger | Payload attributes |
|---|---|---|---|
| Flood Detected | flood_detected |
Water reaches the level the sensor detects | — |
| Flood Cleared | flood_cleared |
Water is no longer detected | — |
| Freeze Detected | freeze_detected |
Internal temperature falls below 40 °F (4 °C) | — |
| Freeze Cleared | freeze_cleared |
Internal temperature rises above 40 °F (4 °C) | — |
| Tamper Detected | tamper_detected |
The device is physically altered | — |
| Tamper Cleared | tamper_cleared |
The tamper condition ends | — |
Indoor temperature/humidity sensor.
| Event | Type value | Trigger | Payload attributes |
|---|---|---|---|
| Temperature Exceeded | temperature_exceeded |
Temperature moves outside its threshold | temperature, threshold_type |
| Temperature Cleared | temperature_cleared |
Temperature returns within threshold | temperature |
| Humidity Exceeded | humidity_exceeded |
Humidity moves outside its threshold | humidity, threshold_type |
| Humidity Cleared | humidity_cleared |
Humidity returns within threshold | humidity |
Indoor air quality monitor.
| Event | Type value | Trigger | Payload attributes |
|---|---|---|---|
| PM2.5 Exceeded | pm25_exceeded |
Particulate level rises above its threshold | pm25_level |
| PM2.5 Cleared | pm25_cleared |
Particulate level returns to normal | pm25_level |
| CO Exceeded | co_exceeded |
Carbon monoxide rises above its threshold | — |
| CO Cleared | co_cleared |
Carbon monoxide returns to normal | — |
| Temperature Exceeded | temperature_exceeded |
Temperature moves outside its threshold | temperature, threshold_type |
| Temperature Cleared | temperature_cleared |
Temperature returns within threshold | temperature |
| Humidity Exceeded | humidity_exceeded |
Humidity moves outside its threshold | humidity, threshold_type |
| Humidity Cleared | humidity_cleared |
Humidity returns within threshold | humidity |
device_online and device_offline apply to every sensor and use the payload documented on Device Online and Device Offline.
device_online is emitted on any device uplink, so for sensors it functions as a heartbeat rather than as a connectivity transition. Sensor device_offline results from a heartbeat timeout rather than from a directly observable transition, so expect it to lag the actual loss of connectivity.
Every sensor event shares this envelope, varying only in data.type and, for threshold events, the added reading.
{
"meta": {
"version": "1.1",
"time": "2026-03-10T14:22:20.258000314Z",
"request_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-1773152538431-contact_senso",
"account_id": "ava1.ring.account.XXXXXXXX"
},
"data": {
"id": "<device_id>_<event_type>_<timestamp>",
"type": "contact_sensor_faulted",
"attributes": {
"source": "<device_id>",
"source_type": "devices",
"timestamp": 1773152538431
},
"relationships": {
"devices": {
"links": {
"self": "/v1/devices/<device_id>"
}
}
}
}
}
Identifiers in this and every other example are placeholders.
| Field | Description |
|---|---|
data.id |
Composed of the device ID, the event type, and the event timestamp. |
data.type |
The event type. Route on this field. |
data.attributes.source |
Device ID of the sensor that emitted the event. Read the device from this field. |
data.attributes.source_type |
Always devices. |
data.attributes.timestamp |
Unix epoch milliseconds when the state change occurred. Use this as the event time. |
data.type and read the device from data.attributes.source. For detection events the event type is the entire signal, because no attribute carries the new state. Do not parse data.id to recover the device ID: its internal structure is not a documented contract and may change without notice.The temperature/humidity sensor and the air quality monitor extend the envelope with the measured reading.
| Attribute | Appears on | Description |
|---|---|---|
temperature |
temperature_exceeded, temperature_cleared |
Measured temperature in degrees Celsius, as a float. |
humidity |
humidity_exceeded, humidity_cleared |
Measured relative humidity as a percentage, as a float. |
pm25_level |
pm25_exceeded, pm25_cleared |
Measured PM2.5 concentration in µg/m³, as a float. |
threshold_type |
temperature_exceeded, humidity_exceeded |
Which bound was crossed, either min or max. Present only on measurements that have both an upper and a lower bound. |
A humidity breach on the temperature/humidity sensor, carrying both the reading and the direction.
"attributes": {
"source": "<device_id>",
"source_type": "devices",
"timestamp": 1787767836967,
"humidity": 61.7,
"threshold_type": "max"
}
The matching cleared event, carrying the reading alone.
"attributes": {
"source": "<device_id>",
"source_type": "devices",
"timestamp": 1787767772030,
"humidity": 55.6
}
threshold_type tells you whether a minimum or a maximum was passed, and the reading tells you by how much only after you look up the bound. Read the configured thresholds from Sensor configuration to recover the limit, and to work out which bound a temperature_cleared or humidity_cleared event returned inside, because cleared events carry no threshold_type.| Guidance | Detail |
|---|---|
Deduplicate on meta.request_id |
Required rather than advisory, because you cannot reconcile duplicates against a canonical event list. |
| Do not infer "no change" from silence | An absent cleared event does not mean the condition persists. |
| Reconcile against status after rapid event sequences | Poll the detection attribute to confirm current state. |
| Request dead letter queue replays within 72 hours | For delivery failures. See Notifications. |
Ring contact sensors report when a door or window opens and closes. The indoor and outdoor variants behave identically across every endpoint and event type.
Status returns contact_detection.faulted, tamper_detection.detected, and sensor_reporting_state.value alongside the common sensor telemetry.
"contact_detection": {
"faulted": true
},
"tamper_detection": {
"detected": false
},
"sensor_reporting_state": {
"value": "active"
}
The payload does not tell you what the sensor is attached to. A door and a window produce the same event from the same device shape, so store that mapping at setup time.
The Ring flood/freeze sensor carries two independent detectors in one device. Status returns flood_detection.faulted, freeze_detection.faulted, tamper_detection.detected, and sensor_reporting_state.value alongside the common sensor telemetry.
"flood_detection": {
"faulted": true
},
"freeze_detection": {
"faulted": true
}
The 40 °F (4 °C) freeze threshold is fixed. It is not configurable, and the event payload carries no temperature reading.
This sensor measures ambient temperature and relative humidity and emits an event when a reading crosses a threshold. It has no detection attributes, so status returns the common sensor telemetry plus temperature and humidity.
You can reach a reading two ways: poll status for the last reported value, or read it from the threshold event when a bound is crossed. Its configured bounds are readable under Sensor configuration.
This monitor measures particulate matter (PM2.5), carbon monoxide, temperature, and humidity. It has no detection attributes, so status returns the common sensor telemetry plus temperature, humidity, pm2_5, and co.
Because it measures temperature and humidity as well as air quality, it emits the four temperature and humidity threshold events in addition to its own four. Nothing in the payload distinguishes this device from a temperature/humidity sensor, so map device IDs to device types on your side.
Temperature and humidity are measured against both an upper and a lower bound, so their events carry a threshold_type. PM2.5 and carbon monoxide are contaminants, measured against an upper limit only. A pm25_exceeded event therefore always means the level rose above its limit, and reports no direction.
Only the temperature and humidity bounds are readable under Sensor configuration.
PM2.5 readings arrive in the threshold event as pm25_level, which is the same measurement the status response calls pm2_5.
"attributes": {
"source": "<device_id>",
"source_type": "devices",
"timestamp": 1787769360072,
"pm25_level": 551.2
}
This device does not support battery reporting, so its battery_status.percentage reads 255 rather than a level.
co_exceeded as a life-safety trigger. Ring's own alarm and notification paths are the authority for carbon monoxide alerts, and this event is not a substitute for them.One handler covers every sensor. Route on data.type, persist before processing, and read the reading only for threshold events.
READING_ATTRIBUTE = {
'temperature': 'temperature',
'humidity': 'humidity',
'pm25': 'pm25_level',
}
def handle_sensor_event(payload):
event_type = payload['data']['type']
account_id = payload['meta']['account_id']
request_id = payload['meta']['request_id']
event_id = payload['data']['id']
attributes = payload['data']['attributes']
device_id = attributes['source']
timestamp = attributes['timestamp']
if already_processed(request_id):
return {'status': 'duplicate'}
# Persist first. A sensor event cannot be retrieved later.
store_event(event_id, event_type, device_id, account_id, timestamp)
measurement = event_type.rsplit('_', 1)[0]
if measurement in READING_ATTRIBUTE:
# Threshold event: the reading is in the payload, the threshold is not.
reading = attributes[READING_ATTRIBUTE[measurement]]
direction = attributes.get('threshold_type')
notify_threshold_crossed(device_id, event_type, timestamp, reading, direction)
else:
# Detection event: the event type is the entire signal.
notify_state_change(device_id, event_type, timestamp)
return {'status': 'processed'}
| Practice | Detail |
|---|---|
| Verify the HMAC signature | In X-Signature, before processing any payload. |
| Respond within 5 seconds | With a 2xx, to avoid a delivery timeout. |
| Persist on receipt | A sensor event you do not store cannot be recovered. |
Deduplicate on meta.request_id |
Required rather than advisory for sensors. |
Route on data.type |
Not on the presence of a state field or a reading. |
Read the device ID from data.attributes.source |
Not by parsing data.id. |
| Poll status for authoritative state | Especially after multiple near-simultaneous events. |
| Store your own device type and placement mapping | The API provides neither. |
Monitor battery_status.percentage |
From status, since no battery event exists. Check capabilities first, and ignore values outside 0–100. |
Read reported_at with any reading |
Status returns the last reported value, not a live one. |
| Filter sensors out | Before calling any media endpoint. |
A Ring Chime is a plug-in indoor speaker that announces events from the Ring cameras and doorbells on a user's account. A chime has no camera and no sensor. It is an output device: it plays audio, and it holds settings that determine what it plays and how loudly.
Chime settings are read-only to partners. Every chime operation available to your app is a read, with one exception: playback, which produces sound and changes nothing stored on the device. No partner operation mutates a chime's configuration.
| Operation | Available to partners |
|---|---|
| Read chime state: online, snooze | Yes |
| Read chime settings: volume, audio slots | Yes |
| Play audio on a chime | Yes, with the Chime Controls capability. Changes no stored state. |
| Change volume or snooze | No. The user controls these in the Ring app. |
| Change the audio in your app's slots | Not through the device API. See Audio slots. |
| Live video, video download, snapshots | No. A chime has no camera. |
A chime is a devices resource, so the device endpoints you already use for cameras and doorbells work unchanged. What differs is the attribute set. Camera-oriented fields are present in the responses but always null, and the meaningful chime data sits under audio.
Chimes are not sensors. They report no physical condition, so nothing in Sensors applies to them. They are also not cameras, so the media streaming and video download endpoints described in Live Video and Media Clips do not apply.
Your app does not need to know which chime model it is talking to. A device accepts the audio your app plays when its capabilities report audio.customizable_slots greater than zero, and that is the only test your code should apply. See Identifying a chime.
For orientation, the Chimes scope group in the developer portal names two device types, and Ring sells both across more than one hardware generation.
| Device type | Description |
|---|---|
| Ring Chime | Plug-in chime that announces doorbell presses and motion events. Supports selectable tones, snooze, and adjustable volume. |
| Ring Chime Pro | Adds a wifi extender for nearby Ring devices and a built-in nightlight. |
Treat those names as product context rather than as a list your app checks against. A model name does not tell you whether a given device has audio slots, and what a chime reports can differ between generations.
Chime access is requested through the Chimes scope group when you create your app. See Configure Your Ring Application for where this sits in the app creation flow.
Selecting the Chimes group requests access to Ring Chime monitoring and control capabilities. On its own, the group covers chime device state and settings events.
Enabling the Chimes group reveals an Additional control capabilities toggle:
| Capability | Description |
|---|---|
| Chime Controls | Play and change chime audio, ring tones, snoozes, and audio settings. |
That description names the chime audio domain the scope covers. It is not a list of endpoints, and the four items in it reach you through three different mechanisms:
| Portal phrase | What your app does | Mechanism |
|---|---|---|
| Play chime audio | Plays audio on the chime | POST /v1/devices/{device_id}/media/audio/playback |
| Change chime audio, ring tones | Supplies the audio occupying your app's slots | Set outside the device API. See Audio slots. |
| Snoozes | Reads whether the user has snoozed the chime | GET /v1/devices/{device_id}/status, read-only |
| Audio settings | Reads the chime volume | GET /v1/devices/{device_id}/configurations, read-only |
Request Chime Controls only if your app plays audio on a chime. An app that reads chime state and settings to reflect them in a dashboard needs the Chimes group alone.
Chimes are returned by the same call as your cameras and doorbells:
GET /v1/devices HTTP/1.1
Authorization: Bearer <access_token>
There is no chime-specific discovery endpoint. A chime appears in the devices collection with the standard relationships to status, capabilities, configurations, and location.
Add ?include=capabilities,configurations,status to sideload those resources and read the slot count, the slot contents, and the online and snooze state for every device in one round trip. The included resources arrive in a top-level included array, matched to each device by the type and id pair in its relationships. See Device Discovery for the matching rule.
{
"data": [
{
"type": "devices",
"id": "<device_id>",
"attributes": {
"name": "Upstairs",
"image_url": "https://app-content.ring.com/shared/images/devices/square_device_images/ChimePro2/chimepro2_3x.png"
},
"relationships": {
"location": {
"data": { "type": "locations", "id": "<location_id>" },
"links": { "related": "/v1/devices/<device_id>/location" }
},
"status": {
"data": { "type": "device-status", "id": "<status_id>" },
"links": { "related": "/v1/devices/<device_id>/status" }
},
"configurations": {
"data": { "type": "device-configurations", "id": "<configurations_id>" },
"links": { "related": "/v1/devices/<device_id>/configurations" }
},
"capabilities": {
"data": { "type": "device-capabilities", "id": "<capabilities_id>" },
"links": { "related": "/v1/devices/<device_id>/capabilities" }
}
}
}
],
"meta": {
"time": "2026-08-17T17:11:12Z"
}
}
The device resource carries no device type or device kind field. attributes contains only name and image_url, so there is no declared value you can switch on to recognize a chime.
Read the capabilities instead. A chime is the device whose audio.customizable_slots is greater than zero:
def plays_partner_audio(capabilities):
audio = capabilities['data']['attributes'].get('audio') or {}
return (audio.get('customizable_slots') or 0) > 0
On a device that is not a chime the audio block is absent, so customizable_slots is missing rather than 0. Both or defaults in the snippet are there for that case — without them the comparison raises an error on the first non-chime device in the collection.
image_url. The path segment happens to name the hardware model, but it addresses a content asset and is not part of the API contract. Use audio.customizable_slots from the capabilities response.GET /v1/devices/<device_id>/capabilities HTTP/1.1
Authorization: Bearer <access_token>
The response keeps the camera-oriented structure and nulls every field that does not apply, then describes the chime under audio.
{
"data": {
"type": "device-capabilities",
"id": "<capabilities_id>",
"attributes": {
"video": {
"configurations": null,
"codecs": null,
"ratio": null,
"max_resolution": null,
"supported_resolutions": null
},
"motion_detection": { "configurations": null },
"image_enhancements": { "configurations": null },
"audio": {
"customizable_slots": 2
}
},
"relationships": {
"configurations": {
"links": { "related": "/v1/devices/<device_id>/configurations" }
},
"data": {}
}
},
"meta": {
"time": "2026-08-17T17:11:12Z"
}
}
2. A value greater than zero is what identifies a device your app can play audio on.audio.customizable_slots is an integer here, reporting how many slots exist. The same field name in the configurations response is an array holding the slots themselves. Read the capability value to size your interface and the configuration value to fill it. The name describes the slots, not your permissions: nothing your app calls changes which audio occupies them.GET /v1/devices/<device_id>/configurations HTTP/1.1
Authorization: Bearer <access_token>
Configurations hold the chime settings that determine what it plays and how loudly: the audio assigned to each slot, and the volume.
{
"data": {
"type": "device-configurations",
"id": "<configurations_id>",
"attributes": {
"motion_detection": { "enabled": null },
"audio": {
"customizable_slots": [
{
"event": "ring-appstore-event-2",
"disabled": false,
"audio_ref": "4d4ec269-0bb2-4312-b7df-5e53c2e613e4",
"audio_url": "https://ringtones-amz.ring.com/ring/appstore/9c62d803-6344-41a1-93c0-49f9331af83c.wav",
"audio_name": "Ring Alert 3"
},
{
"event": "ring-appstore-event-1",
"disabled": false,
"audio_ref": "6150f86e-2631-497c-bdef-1c5c777c10f7",
"audio_url": "https://ringtones-amz.ring.com/ring/appstore/9fef0127-58fb-4f8e-ba87-29a85dc6db33.wav",
"audio_name": "Navi"
}
],
"volume": 4
},
"image_enhancements": {
"color_night_vision": null,
"hdr": null,
"ir_led_night_vision": null,
"auto_zoom_track": null
}
}
},
"meta": {
"time": "2026-08-17T17:11:13Z"
}
}
Your app gets a fixed set of named slots on the chime, one per partner event. Each slot binds an event name to an audio file.
ring-appstore-event-<n>. Chime Pro exposes ring-appstore-event-1 and ring-appstore-event-2.false means the slot plays. Skip a slot whose disabled is true when you choose audio to play.Ring Alert 3.Slots are not returned in event-name order. Sort by event before rendering them.
The slots belong to your integration. Their event names are namespaced to the Ring Appstore, the audio is served from Ring Appstore storage, and the capabilities response describes them as customizable. This is the part of the chime the Chime Controls description means by changing chime audio and ring tones.
/v1/devices/{device_id} writes a slot, and the configurations endpoint is read-only for chimes. Your app reads which audio occupies each slot and plays it.A slot keeps its event name when the audio in it changes. Store event names against your own application events, and resolve the audio_ref from configurations each time you play.
EVENT_SLOTS = {
"door_open": "ring-appstore-event-1",
"package_delivered": "ring-appstore-event-2",
}
def audio_ref_for(configurations, app_event):
slots = configurations['data']['attributes']['audio']['customizable_slots']
target = EVENT_SLOTS[app_event]
for slot in slots:
if slot['event'] == target and not slot['disabled']:
return slot['audio_ref']
return None
audio_ref. It is the value the playback request takes, which makes it the tempting one to store, but it can change when a user re-runs device setup or removes your integration. A stored audio_ref goes stale with nothing to tell you; a stored event name does not.The event name on a slot is a label your app gives meaning to, not a trigger Ring acts on. Nothing plays a slot's audio when the named event occurs, and your app cannot register it to. Audio in your slots is heard only when your app calls playback.
audio.volume is the current chime volume as an integer. Chime Pro reported 4.
audio.volume are not yet specified. Present it as the raw value the API returns rather than converting it to a percentage or a filled meter, so your interface stays correct when the range is published.There is no partner operation that writes chime configurations. Volume is changed by the user in the Ring app. Slot audio is set outside the device API, as described in Audio slots. Neither is reachable from an endpoint your app calls.
Two consequences worth designing for:
GET /v1/devices/<device_id>/status HTTP/1.1
Authorization: Bearer <access_token>
Chime status is deliberately small. It reports reachability and snooze, and nothing else.
{
"data": {
"type": "device-status",
"id": "<status_id>",
"attributes": {
"online": true,
"audio": {
"snooze": {
"active": false,
"until": null
}
}
}
},
"meta": {
"time": "2026-08-17T17:11:13Z"
}
}
active is false.Snooze is set by the user in the Ring app. No partner operation snoozes or unsnoozes a chime, so treat audio.snooze as a condition to respect rather than one to manage.
Volume is not a status attribute. Read it from Chime configurations.
battery_status and no signal_strength. Chimes are mains-powered, so there is no battery to report. Do not reuse sensor status handling that expects those attributes. Chime Pro nightlight state is not exposed through the partner API.Playback is the only chime command available to partners, and it is gated by the Chime Controls capability. It plays audio; it changes nothing about the device.
POST /v1/devices/<device_id>/media/audio/playback HTTP/1.1
Authorization: Bearer <access_token>
Content-Type: application/json
Name the audio to play with its audio_ref, in a JSON:API document whose data.type is audio.
{
"data": {
"type": "audio",
"attributes": {
"audio_ref": "4d4ec269-0bb2-4312-b7df-5e53c2e613e4"
}
}
}
| Field | Type | Required | Description |
|---|---|---|---|
data.type |
String | Yes | Always audio |
data.attributes.audio_ref |
String | Yes | The audio_ref of the audio to play, read from audio.customizable_slots in the device configurations |
The request carries no slot or event name. Playback addresses the audio directly by audio_ref, so read Chime configurations first and take the audio_ref of the slot you want to sound.
GET /v1/devices/{device_id}/configurations and find the slot you want in audio.customizable_slots, matching on event. Skip it if disabled is true.POST /v1/devices/{device_id}/media/audio/playback with that slot's audio_ref.Step 1 is required: the audio_ref the request needs has no other source. Two further reads are worth making, and neither is a precondition the request enforces.
GET /v1/devices/{device_id}/capabilities confirms audio.customizable_slots is greater than zero, which is how you tell a chime from a camera while working through a mixed device collection. If you already hold a known chime's device_id, you can skip it.GET /v1/devices/{device_id}/status reports online and audio.snooze.active. Check both so your app does not try to sound a chime the user has silenced or unplugged.Fetch all three in one request with GET /v1/devices/{device_id}?include=capabilities,configurations,status.
{
"data": {
"type": "audio",
"id": "4d4ec269-0bb2-4312-b7df-5e53c2e613e4",
"attributes": {
"status": "completed"
}
},
"meta": {
"time": "2026-08-17T17:11:14.002266998Z"
}
}
The response is synchronous. data.attributes.status of completed means the chime played the audio, and data.id echoes back the audio_ref you sent, so you can correlate the response with the request that caused it.
audio_ref your app already holds in one of its slots, read from the configurations response. Whether the endpoint accepts an audio_ref that is not currently assigned to one of your slots is not specified.A chime delivers the standard device lifecycle events. These are the same event types documented for cameras and doorbells, with the chime as the event source.
| Event | data.type |
Details |
|---|---|---|
| Device addition | device_added |
Device Addition |
| Device removal | device_removed |
Device Removal |
A device_added event for a chime uses the standard v1.1 envelope:
{
"meta": {
"version": "1.1",
"time": "2026-08-16T22:44:20.294608731Z",
"request_id": "<request_id>",
"account_id": "<account_id>"
},
"data": {
"id": "<device_id>_device_added_1786920259106",
"type": "device_added",
"attributes": {
"source": "<device_id>",
"source_type": "devices",
"timestamp": 1786920259106
},
"relationships": {
"devices": {
"links": {
"self": "/v1/devices/<device_id>"
}
}
}
}
}
Chime webhooks arrive on the same delivery path as every other Ring webhook, with an HMAC-SHA256 signature in the X-Signature header that you must verify before processing the payload. See Notifications for signature verification.
audio.customizable_slots being greater than zero, never on image_url or device name.audio.customizable_slots to learn how many slots exist, then read the array of the same name in configurations to see what occupies them.audio.snooze.active before playing audio. A snoozed chime reflects a deliberate user choice, and no partner operation overrides it.online cannot play audio.battery_status and no signal_strength.X-Signature header on chime webhooks before processing.event names, not audio_ref values: A slot keeps its event name when its audio changes. Resolve the audio_ref from configurations at the point of playback.event name is your label, not a trigger. Your audio sounds only when your app calls playback.The Event History API allows partners to retrieve past events for a specific device. Events include motion detections, live view sessions, doorbell presses, and other device activity. This endpoint returns event metadata only — use the Media Clips and Image Snapshots endpoints to access video clips or images for specific events.
See also: Notifications for real-time event delivery via webhooks.
| Method | Endpoint | Purpose |
|---|---|---|
| GET | https://api.amazonvision.com/v1/history/devices/{device_id}/events |
|
| You are visitor 01402503 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required. |