Tuya Cloud Devices: End-User API Control
Use this skill when the user supplies a Tuya end-user API key (sk-...) and asks to control or
query devices from their Tuya app account. Devices are reached in the cloud, so no local keys or
model-specific datapoint maps are needed. For direct local-TCP control with a confirmed key and
datapoint schema, prefer Tuya Wi-Fi devices.
Prerequisites
Read and follow the Home Link networking and safety rules in ~/docs/devices/home_link.md.
-
A user-supplied end-user API key, applied for at https://tuya.ai/. Treat it as a credential: pass it in a header, never log or echo it.
-
Send
Authorization: Bearer <key>on every call. Pick the base URL from the first two characters aftersk-; it must match the account's region. If the prefix is not listed, ask the user to confirm the key was issued for an international account at https://tuya.ai/. -
Responses share one envelope:
{"success": true, "result": ...}or{"success": false, "code", "msg"}. On1010the key expired — ask for a fresh one. On429back off and honorRetry-After. List endpoints return everything in one response; there is no pagination.
Workflow
- List homes with
GET /v1.0/end-user/homes/all, and rooms withGET /v1.0/end-user/homes/{home_id}/roomswhen the user names one. - Locate the device:
GET /v1.0/end-user/devices/all, or scope to.../homes/{home_id}/devicesor.../homes/room/{room_id}/devices. Each entry inresult.devices[]carriesdevice_id,name,category,category_name,onlineand, under a home scope,room_id. Match the user's words againstcategory_namefirst, then fuzzy-matchname. With multiple matches, list the candidates with their rooms and ask. - Read
GET /v1.0/end-user/devices/{device_id}/detail. Anullresultmeans the device does not exist or the key has no access — stop.online: falsemeans report the device as offline and command nothing.result.propertiesmaps each property code to its current value. - Read the thing model
GET /v1.0/end-user/devices/{device_id}/model. Itsresult.modelfield is a JSON string that needs a second parse.services[].properties[]then defines each property'scode,accessMode(roproperties are read-only — say so) andtypeSpec:value:min,max,stepandscale— the real-world value is the raw number divided by 10^scale.enum: the issued value must be one ofrange.bool/string:true/false, andmaxlenrespectively. Common codes areswitch_led/switch(bool power),bright_value/temp_value(0–1000),temp_set(16–30) andmode(enum) — the model response is always authoritative.
- Issue
POST /v1.0/end-user/devices/{device_id}/shadow/properties/issuewith a body whosepropertiesvalue is a JSON string, not an object — it is double-serialized:{"properties": "{\"switch_led\":true}"}. An emptyresultobject means the command was accepted, not that it took effect. For relative asks ("a bit brighter"), adjust the current value by 10% of the typeSpec range for vague amounts or by the stated amount exactly, clamp to[min, max], honorscaleand round tostep. - Wait 1–2 seconds, re-read the detail and compare the mapped value before reporting success.
Also Available
- Rename:
POST /v1.0/end-user/devices/{device_id}/attributewith{"name": "..."}, max 50 characters. - Weather:
GET /v1.0/end-user/services/weather/recentwithlat,lonandcodes— a URL-encoded JSON array of attribute codes such asw.temp,w.humidity,w.condition,w.conditionNum,w.pressure,w.realFeel,w.uvi,w.windDir,w.windLevel,w.windSpeed, plusw.hour.Nto set the forecast horizon to the next N hours. Response keys are{code}.{index}with index0meaning now. Home coordinates are shaped{"Value": "30.3"}— read the innerValue; ask the user for their city if the home has none set. - Hourly statistics: confirm capability with
GET /v1.0/end-user/statistics/hour/config(items carrydev_id,dp_code,statistic_typeofSUM/COUNT/MAX/MIN), thenGET .../hour/data?dev_id=..&dp_code=..&statistic_type=..&start_time=..&end_time=... Times areyyyyMMddHHand one request spans at most 24 hours; page longer ranges and aggregate. Values return as an array of{time: value}pairs with string values. - Notifications are self-send only — they reach the key's own user, nobody else. SMS and
voice take
{"message": "..."}; mail and push take{"subject": "...", "content": "..."}. Rate and duplicate limits apply (per contact: 15–30 per day, identical content more than twice within 50 seconds is refused). - IPC cloud snapshots (cameras):
POST /v1.0/end-user/ipc/{device_id}/capture/allocatewith body{"capture_json": "<json string>"}— double-serialized like property issue. The string carriesdevice_id,capture_type(PICorVIDEO), optionallypic_count(clamped to 1–5),video_duration_seconds(1–60, default 10) andhome_id. Allocate only reserves an upload slot:result.statusisACCEPTEDorREJECTEDandresultcarriesbucketand the object key(s) (image_object_key, and for VIDEO alsovideo_object_key/cover_image_object_key) — never a media URL; the device still has to capture and upload. Then pollPOST .../capture/resolvewith aresolve_jsonstring echoingdevice_id,capture_type,bucketand the object key(s) from allocate, and passuser_privacy_consent_accepted: true. Resolve returns the link to hand the user: the image/video cloud storage URL indecrypt_image_url/decrypt_video_url.status: NOT_READYmeans keep polling — for PIC sleep 2 s, then resolve every 2 s up to 30 s, then retry up to 3 times 3 s apart; for VIDEO sleepmax(5, video_duration_seconds_effective) + 2s using the clamped duration from the allocate response, then resolve every 2 s up to 120 s, then retry up to 3 times 5 s apart. Withpic_countabove 1, allocate returns per-snapshot coordinates inpic_slots[]— resolve each slot's key separately. A video's cover image can lag the video itself — checkmessage_for_user.
Real-Time Subscription
Property changes and online/offline transitions are also pushed over WebSocket. This is a
long-lived process for an always-on host (a Linux gadget), not a one-shot query. Write a short
Python script using the websockets package:
- URI by key prefix (same mapping as the REST table): AZ
wss://wsmsgs.iot-wus.com, EUwss://wsmsgs.iot-eu.com, INwss://wsmsgs.iot-ap.com, UEwss://wsmsgs.iot-eus.com, WEwss://wsmsgs.iot-weu.com, SGwss://wsmsgs.iot-sea.com. - Auth: an
Authorization: <key>header on the handshake — the same key as REST but without theBearerprefix. - Frames are JSON with two top-level fields,
eventTypeanddata. AdevicePropertyChangecarriesdata.devIdanddata.status[], each withcode,valueandtime(ms); anonlineStatusChangecarriesdata.devId,data.status("online"/"offline") anddata.time. - Codes match thing-model codes. Look their meanings up via the model endpoint instead of guessing from the code string.
- Reconnect with backoff on transient drops. Treat close codes 1002, 1003, 1008 and 1011 as
fatal, and any frame containing
error, a failingerrorCode, or"success": falseas a server error. - For event-driven automation, on a matching trigger issue properties to the action device as in Workflow step 5, and confirm the trigger→action mapping with the user before running it.
Limits
- Control only basic property types (bool, enum, integer, string). Never write
raw,bitmap,structorarrayproperties, or anything absent from the thing model. - No lock/unlock, live video streaming, firmware updates, pairing or device removal — say so and point to the Tuya app.
- Device listing takes one scope at a time (all, home or room). Rate-limit batch writes with a short delay between commands.
- Notifications triggered by device events must cool down at least 30 minutes — property events fire frequently.
- These are the user's own account devices behind an end-user key; never attempt another user's devices.
Sources
- tuya-openclaw-skills — upstream skill this was adapted from.
- Tuya developer docs


