Skip to content

Instantly share code, notes, and snippets.

@mayerwin
Last active February 13, 2026 20:48
Show Gist options
  • Select an option

  • Save mayerwin/018ed9141707d3289a0e762170878e83 to your computer and use it in GitHub Desktop.

Select an option

Save mayerwin/018ed9141707d3289a0e762170878e83 to your computer and use it in GitHub Desktop.
SwitchBot Webhook Deletion Tool (PHP Single-File)

Description

A standalone, single-file PHP tool to easily delete SwitchBot API Webhooks.

This script solves two common pain points when working with the SwitchBot API:

  1. CORS Errors: It acts as a server-side proxy, bypassing browser Cross-Origin Resource Sharing (CORS) restrictions.
  2. Authentication Complexity: It automatically generates the required HMAC-SHA256 signature (sign), nonce, and timestamp (t) headers required for SwitchBot API v1.1.

Features

  • Version Selector: Supports both API v1.0 (Token only) and v1.1 (Token + Secret + Signature).
  • Zero Dependencies: Pure PHP/JS. No Composer or external libraries required.
  • Debug Logging: visible on-screen logs of the HTTP status codes and raw JSON responses from SwitchBot.
  • Secure: Runs server-side; credentials are sent directly to SwitchBot and not stored.

Usage

  1. Download the file and save it as delete_webhook.php.
  2. Upload it to any web server with PHP and cURL support.
  3. Open the file in your browser.
  4. Enter your Open Token and Secret Key.
  • To find these: SwitchBot App > Profile > Preferences > Tap "App Version" 10 times > Developer Options.
  1. Enter the Webhook URL you wish to remove and click Execute Delete.

Requirements

  • PHP 7.4+
  • php-curl extension enabled
<?php
/**
* SwitchBot Webhook Deleter (Single File)
* Features:
* - Version Selector (v1.0 vs v1.1)
* - Automatic Signature Generation for v1.1
* - CORS Bypass via PHP Proxy
* - Full Logging
*/
// --- PHP BACKEND (API PROXY) ---
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_GET['action']) && $_GET['action'] === 'execute') {
// Clean buffer to ensure valid JSON response
ob_clean();
header('Content-Type: application/json');
// 1. Get Input
$input = json_decode(file_get_contents('php://input'), true);
$version = $input['version'] ?? 'v1.1';
$token = trim($input['token'] ?? '');
$secret = trim($input['secret'] ?? '');
$urlToDelete = trim($input['url'] ?? '');
if (empty($token) || empty($urlToDelete)) {
echo json_encode(['error' => 'Missing Token or URL']);
exit;
}
// 2. Prepare Payload (Common for both versions)
// Docs: https://github.com/OpenWonderLabs/SwitchBotAPI#delete-webhook
$payloadData = [
'action' => 'deleteWebhook',
'url' => $urlToDelete
];
$payload = json_encode($payloadData);
// 3. Configure Headers & Endpoint based on Version
$headers = [
"Content-Type: application/json; charset=utf8",
"Content-Length: " . strlen($payload)
];
if ($version === 'v1.0') {
// --- v1.0 Logic ---
// Endpoint: https://api.switch-bot.com/v1.0/webhook/deleteWebhook
// Auth: Authorization header only
$apiUrl = "https://api.switch-bot.com/v1.0/webhook/deleteWebhook";
$headers[] = "Authorization: " . $token;
} else {
// --- v1.1 Logic ---
// Endpoint: https://api.switch-bot.com/v1.1/webhook/deleteWebhook
// Auth: Authorization + Sign + Nonce + t
if (empty($secret)) {
echo json_encode(['error' => 'Secret Key is required for v1.1']);
exit;
}
$apiUrl = "https://api.switch-bot.com/v1.1/webhook/deleteWebhook";
// Generate Signature
$nonce = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
$t = time() * 1000;
$stringToSign = $token . $t . $nonce;
$sign = strtoupper(base64_encode(hash_hmac('sha256', utf8_encode($stringToSign), utf8_encode($secret), true)));
$headers[] = "Authorization: " . $token;
$headers[] = "sign: " . $sign;
$headers[] = "nonce: " . $nonce;
$headers[] = "t: " . $t;
}
// 4. Execute cURL
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
// Optional: SSL Verify. Set to false ONLY if your server has certificate issues.
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
// Debug info: Mask sensitive data for logs
$debugHeaders = $headers;
// We don't want to log the full token/secret in plain text if we can avoid it,
// but useful for debugging connectivity.
curl_close($ch);
// 5. Return Response
if ($curlError) {
echo json_encode(['error' => 'cURL Error: ' . $curlError]);
} else {
echo json_encode([
'version_used' => $version,
'endpoint_used' => $apiUrl,
'http_code' => $httpCode,
'response' => json_decode($response),
'payload_sent' => $payloadData
]);
}
exit;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SwitchBot Webhook Tool</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; background-color: #121212; color: #e0e0e0; }
.card { background: #1e1e1e; padding: 2rem; border: 1px solid #333; border-radius: 8px; }
h2 { margin-top: 0; color: #ff6b6b; border-bottom: 1px solid #333; padding-bottom: 1rem; }
.form-group { margin-bottom: 1.5rem; }
label { display: block; margin-bottom: 0.5rem; color: #aaa; font-size: 0.9rem; }
input[type="text"], select { width: 100%; padding: 12px; background: #2d2d2d; border: 1px solid #444; color: #fff; box-sizing: border-box; font-family: monospace; font-size: 1rem; border-radius: 4px; }
input:focus, select:focus { border-color: #ff6b6b; outline: none; }
button { background-color: #ff6b6b; color: #000; border: none; padding: 14px 24px; cursor: pointer; font-weight: bold; width: 100%; margin-top: 1rem; border-radius: 4px; transition: background 0.2s; }
button:hover { background-color: #ff5252; }
button:disabled { background-color: #555; cursor: not-allowed; color: #888; }
#logs { margin-top: 2rem; padding: 1rem; background: #000; border: 1px solid #333; height: 400px; overflow-y: auto; white-space: pre-wrap; color: #00ff00; font-family: monospace; font-size: 0.85rem; border-radius: 4px; }
.log-entry { margin-bottom: 1em; border-bottom: 1px solid #222; padding-bottom: 0.5em; }
.log-err { color: #ff5252; }
.log-warn { color: #ffd700; }
</style>
</head>
<body>
<div class="card">
<h2>DELETE WEBHOOK</h2>
<div class="form-group">
<label for="version">API Version (Try v1.1 first)</label>
<select id="version" onchange="toggleSecretField()">
<option value="v1.1">v1.1 (Recommended - Requires Secret)</option>
<option value="v1.0">v1.0 (Legacy - Token Only)</option>
</select>
</div>
<div class="form-group">
<label>Token (Open Token)</label>
<input type="text" id="token" placeholder="Your Open Token">
</div>
<div class="form-group" id="secret-group">
<label>Secret Key</label>
<input type="text" id="secret" placeholder="Your Secret Key">
</div>
<div class="form-group">
<label>Webhook URL to Delete</label>
<input type="text" id="url" placeholder="https://your-domain.com/hook">
</div>
<button id="deleteBtn" onclick="executeDelete()">EXECUTE DELETE</button>
<div id="logs">System Ready. Select version and enter credentials.</div>
</div>
<script>
// UI Helper: Hide secret field if v1.0 is selected
function toggleSecretField() {
const ver = document.getElementById('version').value;
const secretGroup = document.getElementById('secret-group');
if (ver === 'v1.0') {
secretGroup.style.opacity = '0.5';
document.getElementById('secret').disabled = true;
} else {
secretGroup.style.opacity = '1';
document.getElementById('secret').disabled = false;
}
}
function log(msg, type = 'info') {
const logs = document.getElementById('logs');
const ts = new Date().toLocaleTimeString();
let colorClass = type === 'error' ? 'log-err' : '';
const entry = `<div class="log-entry ${colorClass}">[${ts}] ${msg}</div>`;
logs.innerHTML = entry + logs.innerHTML;
}
async function executeDelete() {
const version = document.getElementById('version').value;
const token = document.getElementById('token').value.trim();
const secret = document.getElementById('secret').value.trim();
const url = document.getElementById('url').value.trim();
const btn = document.getElementById('deleteBtn');
if (!token || !url) return log("Error: Token and URL are required.", 'error');
if (version === 'v1.1' && !secret) return log("Error: v1.1 requires Secret Key.", 'error');
btn.disabled = true;
btn.textContent = "Communicating...";
try {
log(`Attempting DELETE via API ${version}...`);
const response = await fetch('?action=execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ version, token, secret, url })
});
const data = await response.json();
if (data.error) {
log(`Server Error: ${data.error}`, 'error');
} else {
log(`HTTP Code: ${data.http_code}`);
log(`Endpoint Used: ${data.endpoint_used}`);
log(`Response Body:\n${JSON.stringify(data.response, null, 2)}`);
// Check for success code 100
if (data.response && data.response.statusCode === 100) {
log("SUCCESS: Webhook deleted successfully!");
} else if (data.response && data.response.statusCode === 190) {
log("FAILURE: Device internal error or Signing error. Check Secret/Token.", 'error');
} else {
log("FAILURE: See response body for details.", 'error');
}
}
} catch (e) {
log(`JavaScript Error: ${e.message}`, 'error');
} finally {
btn.disabled = false;
btn.textContent = "EXECUTE DELETE";
}
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment