Skip to content

Navigation Menu

Sign in
Sign up

deleteKey succeeds (statusCode 100) but expired Keypad passcodes are never removed from keyList #498

Open
Assignees
Labels
bugSomething isn't working

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:

  1. Calls GET /v1.1/devices and reads keyList for each Keypad
  2. Selects passcodes whose status is expired
  3. Sends deleteKey with {"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

  1. Create a time-limited passcode on a Keypad via createKey (commandType: command,
    command: createKey, type: timeLimit) with a short validity window.
  2. Wait until the validity window has passed. GET /v1.1/devices now reports the
    passcode in keyList with status: "expired".
  3. Send deleteKey with parameter: {"id": "<the id from keyList>"}.
    -> Response is statusCode: 100 (success).
  4. Wait, then call GET /v1.1/devices again.
    -> The passcode with the same id is STILL present in keyList with
    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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    AltStyle によって変換されたページ (->オリジナル) /