ringpartner
Unofficial Python SDK for the Ring Partner API (Amazon Vision, https://api.amazonvision.com).
Ring publishes REST docs and a browser JavaScript WHEP example, but no official Partner SDK.
ringpartner covers the parts every integration ends up rewriting:
- a typed REST client, sync (
RingClient) and asyncio (AsyncRingClient), with JSON:APIincludedresolution, typed errors, 429 back-off that honoursRetry-After, and the two-step media download; - server-side WHEP live video (aiortc), which yields decoded frames for computer vision;
- webhook verification (
X-Signature: sha256=<hex>) and v1.1 payload parsing; - a signed webhook simulator, because the Developer Playground does not deliver webhooks;
- record/replay fixtures, so you can develop and test offline once your Playground token expires;
- a
ringpartnerCLI.
Not affiliated with or endorsed by Ring LLC or Amazon. "Ring" is a trademark of its owner.
Install
pip install ringpartner # REST, webhooks, simulator, fixtures (only needs httpx)
pip install 'ringpartner[whep]' # adds live video: aiortc, PyAV, numpy
Python 3.10 or newer.
Quickstart
Runnable scripts are in examples/: list devices and history with event_types
(list_devices_history.py), WHEP frames into OpenCV
(whep_to_opencv.py), and a stdlib webhook receiver tested with
the signed simulator (webhook_receiver.py).
Sync client
from ringpartner import RingClient
with RingClient("ACCESS_TOKEN") as ring:
for d in ring.devices(): # ?include=status,capabilities,configurations,location
print(d.id, d.name, d.online, d.is_chime, d.is_encrypted)
for ev in ring.iter_history(d.id, event_types=["motion.human", "ding"]):
print(ev.start, ev.event_type, ev.is_third_party_reviewed)
img = ring.snapshot(d.id, between=(start_ms, end_ms)) # or at=<epoch ms>
img.save("front.jpg")
token can be a string or a zero-argument callable. For linked accounts use
RefreshTokenProvider(refresh_token, client_id, client_secret): it refreshes the 4-hour access
token shortly before it expires and is thread-safe.
Errors are typed: RingUnauthorized (401), RingForbidden (403), RingNotFound (404),
RingRateLimited (429, retried automatically up to max_retries), RingDeviceUnavailable (503),
and RingAPIError for everything else. Each carries .status, .title, .detail, .errors.
Async client
from ringpartner import AsyncRingClient
async with AsyncRingClient(token) as ring:
devices = await ring.devices()
async for ev in ring.iter_history(devices[0].id, ["motion"]):
...
token may also be an async callable here.
Live video frames for computer vision (WHEP)
from ringpartner import AsyncRingClient
from ringpartner.whep import WhepSession # needs the [whep] extra
async with AsyncRingClient(token) as ring:
async with WhepSession(ring, device_id, max_seconds=25) as live:
print(live.answer.video_codec) # read from the SDP answer, e.g. "H264/90000"
async for frame in live.frames(fps=2):
bgr = frame.to_ndarray(format="bgr24") # numpy array, ready for OpenCV / a model
await live.record("clip.mp4") writes the stream to a file. Pass audio=True for one-way audio
and component_id="1" to pick a camera on a multi-camera device. The session is closed with
DELETE <Location> on exit. import ringpartner never imports aiortc; ringpartner.WhepSession
is loaded lazily.
Verifying webhooks
from ringpartner import verify_and_parse, InvalidSignature
@app.post("/webhooks/ring")
async def ring_webhook(request):
raw = await request.body() # the raw bytes, not re-serialised JSON
try:
event = verify_and_parse(SIGNING_KEY, raw, request.headers.get("X-Signature"))
except InvalidSignature:
return Response(status_code=401)
if seen(event.dedupe_key): # Ring retries; dedupe on meta.request_id
return Response(status_code=200)
if event.type == "motion_detected" and event.sub_type == "human":
...
return Response(status_code=200) # respond within 5 s
WebhookEvent exposes type, device_id (device events only), device_ids (subscription
events list covered devices here), sub_type, component_ids, account_id, request_id,
timestamp and the full raw document.
Simulating webhooks
The Developer Playground simulates live-view sessions but does not deliver webhooks, so without real hardware you cannot exercise your motion or doorbell handler end to end. The simulator builds payloads in the documented v1.1 shape for all 10 event types, signs them with your HMAC key, and POSTs them, so your production handler runs unchanged, signature check included.
from ringpartner import build_event, deliver
payload = build_event("motion_detected", device_id, sub_type="human", component_ids=["0"])
deliver("http://localhost:8000/webhooks/ring", payload, SIGNING_KEY)
Device events get data.id = <device>_<sub_type>_<ts> for motion and <device>_<event_type>_<ts>
for the rest (e.g. <device>_button_press_<ts>). app_integration_* uses source_type: "users"
with no relationships. subscription_* uses source: "subscriptions", source_id, plan_id
and relationships.devices.data. signed_request(payload, key) returns (body, headers) if you
would rather send the request through your own test client.
Record once, replay offline
Playground access tokens last about 30 minutes and live sessions 30 to 60 seconds, so iterating against the live API is slow. Record real responses once and replay them as often as you like, with no token and no network:
from ringpartner import RingClient, RecordingTransport, ReplayTransport
with RingClient(token, transport=RecordingTransport("fixtures/playground")) as ring:
ring.devices(); ring.history(device_id); ring.snapshot(device_id, at=ts)
with RingClient("unused", transport=ReplayTransport("fixtures/playground")) as ring:
ring.devices() # served from disk
AsyncReplayTransport does the same for AsyncRingClient. Fixtures never contain your bearer
token. The pre-signed media Location is rewritten to https://media.replay.invalid/..., so its
security_token is never written to disk either. A request with no recorded fixture gets a
JSON:API 404.
CLI
The token comes from --token or RING_ACCESS_TOKEN and is never printed.
export RING_ACCESS_TOKEN=... # e.g. from the Developer Playground
ringpartner devices # id, name, online, encrypted
ringpartner history DEVICE_ID --types motion.human,ding
ringpartner snapshot DEVICE_ID --out front.jpg # latest image in the last 10 min
ringpartner snapshot DEVICE_ID --out at.jpg --at-ms 1699457230000
ringpartner record DEVICE_ID --out live.mp4 --seconds 25 --audio # needs [whep]
ringpartner capture-fixtures --dir fixtures/playground
ringpartner simulate motion_detected --device DEVICE_ID --sub-type human \
--url http://localhost:8000/webhooks/ring --key "$RING_HMAC_KEY"
ringpartner verify --key "$RING_HMAC_KEY" --signature "sha256=..." < body.json
capture-fixtures calls devices, users/me, and for each device its detail, status,
capabilities, configurations, location and history, plus one snapshot. If an individual call
fails it reports the failure and moves on.
Gotchas from the docs (handled for you)
- History filters use
event_types, plural and comma-separated, with composite values such asmotion.human. The singularevent_typeis silently ignored. Pagination followslinks.next(page[key]), and an empty page still carries anextlink, so the iterator stops on the first empty page. - Media downloads are two steps. The POST returns
303 See Otherto a pre-signed URL on another host, and that URL must be fetched without theAuthorizationheader. Video can return206 Partial Content(Media.length_ms,Media.partial). - WHEP: finish ICE gathering before the POST. A partial offer causes connectivity failures
(and usually an undocumented HTTP 500). aiortc gathers inside
setLocalDescription. - WHEP is
recvonlyonly.sendrecvandsendonlyare rejected. The POST isapplication/sdp, returns201plus aLocationheader, and you close withDELETE. - Sessions are capped at 30 s for battery devices and 60 s for line-powered ones. The default
max_secondsis 27. Reconnect if you need longer. - Read the live codec from the SDP answer, not from
capabilities.video.codecs(an HEVC camera may answer H264). sub_typeis an open vocabulary:motionwithout Smart Alerts, andhuman,vehicle,package_delivery,other_motionwith it. Ring can add values, so pass unknown ones through.- TAKE/E2EE encrypted devices:
configurations.e2e_encryption.enabledis a JSON boolean. When it is true, media requests succeed but return content you cannot decode. CheckDevice.is_encryptedbefore running CV. - There is no device type field. A chime is a device whose
audio.supported_actionscontainschime.play(Device.is_chime). Do not parseimage_url. - Multi-camera devices (Ring Elite): pass exactly one
component_idfor snapshots and clips. - Snapshots in the Developer Playground: a range request (
between=, or the CLI default of "latest in the last 10 min") returned403 time range not within authorized boundariesfor us.at=<history event start>works.
Development
uv venv --python 3.12 && uv pip install -e '.[whep]' pytest pytest-asyncio respx ruff
.venv/bin/pytest && .venv/bin/ruff check .
License
Apache License 2.0. See LICENSE. Copyright 2026 Brandon Garcia.
Metadata
Release files for ringpartner 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ringpartner-0.1.0.tar.gz | 37.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ringpartner-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 70.5 kB
Release files / ringpartner-0.1.0.tar.gz
| Download URL | ringpartner-0.1.0.tar.gz |
|---|---|
| Size | 37.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d32e6258833890ec9b03eb0a40a0f0a5ccbdda601a59b9656b2a282b7ea45e77
|
|
BLAKE2b-256 checksum How to use checksums |
ded8dfb6a73cf47a9ce5cd9f15be7644c1d6c4bedacc0bc39ae55eeb280fc404
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / ringpartner-0.1.0-py3-none-any.whl
| Download URL | ringpartner-0.1.0-py3-none-any.whl |
|---|---|
| Size | 33.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d139f1739c3d51c6e41f1b210d5faf744c4ed607e071921f8a44a8fa595464ff
|
|
BLAKE2b-256 checksum How to use checksums |
770bea5b008eac4a2c87d8a59241f4e524354149170f62afae1c801124125c9b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|