-
Notifications
You must be signed in to change notification settings - Fork 84
deleteKey succeeds (statusCode 100) but expired Keypad passcodes are never removed from keyList #498
Description
Analysis
When deleting an expired passcode from a Keypad / Keypad Touch via the deleteKey
command, the API responds with statusCode: 100 (success), but the passcode is NOT
actually removed from the device. The same key id is still returned by
GET /v1.1/devices on the following day.
We run a nightly cleanup job that:
- Calls
GET /v1.1/devicesand readskeyListfor each Keypad - Selects passcodes whose
statusisexpired - Sends
deleteKeywith{"commandType":"command","command":"deleteKey","parameter":{"id":"<id>"}}
Results from two consecutive nights (2026年07月21日 and 2026年07月22日, 6 accounts):
- Night 1: 151 delete commands sent, all returned
statusCode: 100, 0 errors - Night 2: 150 delete commands sent, all returned
statusCode: 100, 0 errors - 149 of the 150 keys deleted on night 2 were THE SAME key ids already deleted on night 1
The number of expired passcodes per device did not decrease. For two accounts the
count was completely unchanged (102 -> 102, 70 -> 70).
This is not an isolated device sync failure: it happens uniformly across all
accounts and all Keypad devices we manage.
Some of these expired passcodes were created in March 2023 and are still present
today, so they are clearly never removed automatically either.
Note: on our devices, expired passcodes ARE listed in keyList (unlike the case
reported in #479), so we do have their ids. The problem is that deleting them by
id has no effect.
Expected Behavior
Either:
(a) deleteKey removes the expired passcode from the device, so it no longer
appears in keyList on subsequent GET /v1.1/devices calls, or
(b) if expired passcodes cannot be deleted via the API by design, deleteKey
should return a distinct, documented error code instead of statusCode: 100,
so that callers can detect the situation instead of silently retrying forever.
Additionally, the documentation for deleteKey should state whether expired
passcodes are deletable, and whether expired passcodes are ever removed
automatically by the device or the cloud.
Steps To Reproduce
- Create a time-limited passcode on a Keypad via
createKey(commandType: command,
command: createKey, type: timeLimit) with a short validity window. - Wait until the validity window has passed.
GET /v1.1/devicesnow reports the
passcode inkeyListwithstatus: "expired". - Send
deleteKeywithparameter: {"id": "<the id from keyList>"}.
-> Response isstatusCode: 100(success). - Wait, then call
GET /v1.1/devicesagain.
-> The passcode with the same id is STILL present inkeyListwith
status: "expired".
Logs
# Nightly cleanup job, run 1 (2026年07月20日 22:37 UTC) # For each expired passcode we send deleteKey and check the response statusCode. delete stale key deviceId=DEVICE_A keyId=14 name=<redacted> status=expired createTime=2026年04月17日T08:50:12.000Z delete stale key deviceId=DEVICE_A keyId=15 name=<redacted> status=expired createTime=2026年04月25日T04:29:56.000Z delete stale key deviceId=DEVICE_A keyId=16 name=<redacted> status=expired createTime=2026年05月09日T07:01:59.000Z delete stale key deviceId=DEVICE_A keyId=17 name=<redacted> status=expired createTime=2026年05月09日T08:10:35.000Z delete stale key deviceId=DEVICE_A keyId=18 name=<redacted> status=expired createTime=2026年05月28日T00:16:12.000Z ... (151 delete commands in total, every one returned statusCode: 100, zero errors) summary run 1: account=ACCOUNT_1 candidates=102 deleted=50 failed=0 remaining=52 summary run 1: account=ACCOUNT_2 candidates=70 deleted=50 failed=0 remaining=20 summary run 1: account=ACCOUNT_3 candidates=164 deleted=50 failed=0 remaining=114 # Nightly cleanup job, run 2 (2026年07月21日 19:30 UTC, ~21 hours later) # The SAME key ids are still present in keyList and are selected again. delete stale key deviceId=DEVICE_A keyId=14 name=<redacted> status=expired createTime=2026年04月17日T08:50:12.000Z delete stale key deviceId=DEVICE_A keyId=15 name=<redacted> status=expired createTime=2026年04月25日T04:29:56.000Z delete stale key deviceId=DEVICE_A keyId=16 name=<redacted> status=expired createTime=2026年05月09日T07:01:59.000Z delete stale key deviceId=DEVICE_A keyId=17 name=<redacted> status=expired createTime=2026年05月09日T08:10:35.000Z delete stale key deviceId=DEVICE_A keyId=18 name=<redacted> status=expired createTime=2026年05月28日T00:16:12.000Z ... (150 delete commands in total, every one returned statusCode: 100, zero errors) summary run 2: account=ACCOUNT_1 candidates=102 deleted=50 failed=0 remaining=52 summary run 2: account=ACCOUNT_2 candidates=70 deleted=50 failed=0 remaining=20 summary run 2: account=ACCOUNT_3 candidates=157 deleted=50 failed=0 remaining=107 # 149 of the 150 key ids deleted in run 2 were identical to those already # "successfully" deleted in run 1. # For ACCOUNT_1 and ACCOUNT_2 the candidate count did not change at all. # Snapshot of GET /v1.1/devices keyList for one device (before run 1) device=DEVICE_B total_keys=64 status_counts={"expired":63,"normal":1} oldest_createTime=2023年03月29日T04:03:26.000Z device=DEVICE_C total_keys=56 status_counts={"expired":52,"normal":4} oldest_createTime=2023年03月07日T10:25:07.000Z device=DEVICE_D total_keys=48 status_counts={"expired":42,"normal":6} oldest_createTime=2026年04月13日T02:23:23.000Z # Note: our logs record only the response statusCode (we treat statusCode != 100 # as a failure). Full response bodies were not retained. We can add full-body # logging and provide it if needed.
Configuration
We call the Open API directly; there is no config file. The exact requests are: # 1. Fetch device list (including keyList) GET https://api.switch-bot.com/v1.1/devices Headers: Authorization: <token> # redacted sign: <HMAC-SHA256> # redacted t: <epoch millis> nonce: <random string> Content-Type: application/json # 2. Delete one expired passcode POST https://api.switch-bot.com/v1.1/devices/{deviceId}/commands Headers: (same as above) Body: { "commandType": "command", "command": "deleteKey", "parameter": { "id": "14" } } Response we receive: { "statusCode": 100, "message": "success", "body": {} } The `id` value is taken directly from `body.deviceList[].keyList[].id` of the `GET /v1.1/devices` response, converted to a string. Question: is `parameter` supposed to contain any field other than `id` (e.g. `name` or `type`)? The documentation does not make this clear, and since the API returns statusCode 100 we have no signal that anything is wrong.
Environment
- OS: Debian 12 (bookworm) — container image
node:22-bookworm-slim - Runtime: Node.js 22.x
- HTTP client: axios ^1.7.9 (via @nestjs/axios ^3.1.3)
- Framework: NestJS 10.4.22
- Hosting: Google Cloud Platform, App Engine flexible environment (asia-northeast1)
- SwitchBot Open API version: v1.1
- Devices: SwitchBot Keypad and Keypad Touch (multiple units across 6 separate
accounts/tokens) - Observed on: 2026年07月21日 and 2026年07月22日 (JST)
Additional Context
No response