Ring does not provide an official API, but it uses one internally. In this article we will try to address that.
The python-ring-doorbell project has reverse engineered this API into a nice Python library, which we will use here. We will wrap this library for two use cases
- Collecting the an OAUTH2 access and refresh tokens to call the actual APIs
- Wrapping it into a Flask server to expose a REST API to trigger video downloads
The component responsible in our architecture for these tasks is the connector

Setting up the python project
If you are familiar with python, you can jump to the next chapter.
We will use python 3 here, so you will need to install that. You can check the details for your OS here for a mac with brew simply run
brew install python3
Python provides a concept of virtual envs which is a sort of superset of the project level package management known from javascript or other languages. It is advisable to activate this by
python3 -m venv venv
source venv/bin/activate
Packages in python are managed via the requirements.txt file, and we will create one with the dependencies we need here. We will only use two here, the aforementioned ring_doorbell
and config managment via python-decpouple
.
#requirements.txt
ring_doorbell==0.7.2
python-decouple
To install these requirements into your python virtual env, run pip3 install -r requirements.txt
We will create the package (direcxtory) ringConnector to scope the modules we are creating next.
mkdir ringConnector
Authorization module
To call the internal Ring API we will need to provide an OAUTH2 token. Here, the python-ring-doorbell lib already provides most of the code, so we have little to do. You can find the final code of this module in GitHub.
Create the module (file) ringConnector/authorization.py and add the following content. Basically we create new Auth
and provide it a callback for token refresh (token_updated
) and second factor authorisation (otp_callback
)
# ringConnector/authorization.py
oauth_file = Path(config("OAUTH_FILE", default="oauth-authorization.json"))
"""
Asks for the user credentials and creates the oauth json file
"""
def writeAuthFile():
logging.warn(f"will create new oauth json file at {oauth_file}")
username = input("Please enter your ring Username: ")
password = getpass.getpass("Please enter your ring Password: ")
auth = Auth("Ringface", None, token_updated)
try:
auth.fetch_token(username, password)
except MissingTokenError:
auth.fetch_token(username, password, otp_callback())
def token_updated(token):
oauth_file.write_text(json.dumps(token))
logging.warn(f"new token file created")
def otp_callback():
auth_code = input("Please enter your 2FA code that was sent to you: ")
return auth_code
We now wrap the above code in an shell launcher called startCreateAuthFile.py
#!/usr/bin/env python3
from ringConnector.authorization import writeAuthFile
writeAuthFile()
set the chmod +x
flag, and run it: ./startCreateAuthFile.py
. The script will ask you for your Ring user, password, and 2fa token, respectively, and create a local oauth-authorization.json file, with the OAUTH2 Access Token and Refrest Tokens. Please keep this file private, as it contains access rights to your Ring device.
# oauth-authorization.json
{
"access_token": "xxx.yyy.zzz",
"expires_in": 14400,
"refresh_token": "xxx.yyy.zzz",
"scope": ["client"],
"token_type": "Bearer",
"expires_at": 1697138450.575915
}
With the this authorization token in stash, we can now proceed to invoking the Ring internal API.
Download API
We plan to create two REST Endpoints for our purposes:
- download videos for today
- download videos for any day, which have not been downloaded yet
As before, we will base the core logic of this on the excellent python-ring-doorbell lib, and we will put a Flask server as REST handler. The final code is available on GitHub.
To use the Flask server, we need to declare it in out requirements and reimport the dependencies by pip3 install -r requirements.txt
#requirements.txt
ring_doorbell==0.7.2
python-decouple
Flask
We will create a ringConnector/server.py
module, for the REST endpoints, with the following content
@app.route('/connector/download/today')
def downloadForToday():
logging.debug(f"Downloading todays events")
eventsList = downloadDaysDingVideos()
return jsonify(eventsList)
@app.route('/connector/download/<dayString>', methods=["POST"])
def downloadForDay(dayString):
downloadedEventsRingIds = request.json
logging.debug(f"Downloading {dayString}. Will not re-download events {downloadedEventsRingIds}")
# assert dayString == request.view_args['day']
dayToDownload = datetime.datetime.strptime(dayString, '%Y%m%d').date()
eventsList = downloadDaysDingVideos(dayToDownload = dayToDownload, downloadedEventsRingIds = downloadedEventsRingIds)
logging.debug(f"downloaded {len(eventsList)} new events for {dayToDownload}")
return jsonify(eventsList)
Note we are exposing a GET endpoint on line 1, and a POST endpoint on line 7. The reason for the POST endpoint is, that we will be passing it the list of already downloaded videos for a given day. A use case is when you poll your Ring doorbell every hour, or after a restart to get just the difference. This will save significant runtime, as downloading mp4 files may result in long runtimes. In both of the above API, the call is propagated to the downloadDaysDingVideos
method in the core module.
The ringConnector/core.py module will actually run the bulk of the work.
def downloadDaysDingVideos(dayToDownload=date.today(), dirStructure=DEFAULT_DIR_STUCTURE, downloadedEventsRingIds = []):
logging.debug(f"exising events will not be downloaded: {downloadedEventsRingIds}")
ring = getRing()
downloadedEvents = []
logging.info(f"Importing all ding event videos for {dayToDownload}")
devices = ring.devices()
for doorbell in devices['doorbots']:
for event in doorbell.history(limit=300, kind='ding'):
if (dayToDownload == None or event['created_at'].date() == dayToDownload):
if str(event["id"]) in downloadedEventsRingIds:
logging.debug(f"event {event['id']} already present, will not re-download")
elif event["recording"]["status"] != "ready":
logging.debug(f"event {event['id']} is not yet ready, and can not be downloaded")
else:
logging.debug(f"event {event['id']} will be downloaded")
eventJson = downloadAndSaveEvent(event, doorbell, dirStructure, dayToDownload)
downloadedEvents.append(eventJson)
return downloadedEvents
Here we iterate over the ring devices (9) in case more than one doorbell is installed. We will disregard events downloaded previously (13). Events that are not in ready state (15) are still being finalised by the ring servers, and will not provide a valid mp4 file. For all the rest, we run the download, and save the file:
def downloadAndSaveEvent(event, doorbell, dirStructure, dayToDownload):
id = event['id']
eventName = event['created_at'].strftime("%Y%m%d-%H%M%S")
filename = f"{dirStructure.videos}/{eventName}"
videoFileName = filename+".mp4"
eventJson = {
'ringId':str(id),
'date': dayToDownload.strftime("%Y%m%d"),
'eventName': eventName,
'createdAt':event['created_at'].strftime('%Y-%m-%dT%H:%M:%S.%fZ'), #json formatted event time
'answered': event['answered'],
'kind': event['kind'],
'duration': event['duration'],
'videoFileName': videoFileName,
'status': 'UNPROCESSED'
}
with open(filename + ".json", 'w') as eventDetails:
json.dump(eventJson, eventDetails)
# short after the event, the video is not yet be available for download
# on unavailablility retry for 100 sec
i = 1
while True:
try:
doorbell.recording_download(id,filename=videoFileName,override=True)
break
except requests.exceptions.HTTPError as err:
logging.info(f"{videoFileName} is not yet available. will retry in 10 sec. Err: {err.response.status_code}")
i = i + 1
if i == 10:
break
else:
time.sleep(10)
return eventJson
The loop (26) ensures, that when we start the download right after an Ring Event was captured, we will wait until the recording is finalised, and available for download. Capturing this trigger event (someone pressed the doorbell) will be a subject of the post next week.
We now wrap the above code in an shell launcher called startDownload.py
#!/usr/bin/env python3
import logging
import datetime
import sys
from ringConnector import core
logging.getLogger().setLevel(logging.INFO)
devices = core.listAllDevices()
logging.info(f"found devices {devices}")
print ('Argument List:', str(sys.argv))
if len(sys.argv) > 1:
dayToDownloadParam = sys.argv[1]
logging.info(f"will download {dayToDownloadParam}")
dayToDownload = datetime.datetime.strptime(dayToDownloadParam, '%Y-%m-%d').date()
res = core.downloadDaysDingVideos(dayToDownload = dayToDownload)
else:
logging.info('will download today')
res = core.downloadDaysDingVideos()
logging.info(res)
set the chmod +x
flag, and run it: ./
. startDownload
.py
(venv) MBP:ringface-connector$ ./startDownload.py
INFO:root:found devices {'stickup_cams': [<RingStickUpCam: Downstairs>, <RingStickUpCam: XXXX>], 'chimes': [<RingChime: YYY>], 'doorbots': [<RingDoorBell: ZZZ>], 'authorized_doorbots': []}
INFO:root:will download today
INFO:root:Importing all ding event videos for 2022-10-13
INFO:root:[{'ringId': '7289369076883314527', 'date': '20221013', 'eventName': '20221013-091454', 'createdAt': '2022-10-13T09:14:54.000000Z', 'answered': False, 'kind': 'ding', 'duration': 33.0, 'videoFileName': './data/videos/20221013-091454.mp4', 'status': 'UNPROCESSED'}]
At this point, you should have a local file in your ./data/videos
directory, which you can skim for content.

In our post next week, we will look at the downloaded file, how to extract the relevant frames, how to find faces in those frame, and how to recognise persons (classify faces).