Quick link for guest to toggle garage door from their phone?

Is there any way to provide some sort of quick link or shortcut that a guest can use to toggle open my garage door from their phone? I don't want to make them install the HA app since they'll just be here temporarily. My HA is behind Tailscale (guest already uses TS) so I'm not worried about this being open to the world or anything.

Any ideas? Thanks!

A zigbee button outside the garage?
Otherwise If they are on your tailscale, can't they just use a browser and IP address to get into HA?
I know with ZeroTier you can.

That's a good idea but I can't figure out how to create a user that doesn't have access to everything in my house. I just want them to have access to the garage door button.

I would use webhook for that and not look into user model as that is not suitable for this setup

I'm not familiar with webhooks, can you explain a bit more about this? Thanks!

Yes, in short:
Create the automation that carries out the action
Create the webhook trigger and give an unique id that's unguesable

Share the linkt to the webhook to your guest
https://myhass.com/api/webhook/webhookid

Here some idea to understand:

This worked great, thank you! For anyone else who stumbles on this I added the webhook itself by opening the automation then adding a new Trigger, selecting 'Webhook', then setting the webhook id. I had to manually edit the YAML for that webhook to add 'GET' below 'POST' and 'PUT' or it wouldn't work.

Appreciate the help!

Hi,

The webhook is indeed the best option.
An idea is to program a NFC tag (with NFC tools app on mobile) incl your webhook URL

There is no way. All or nothing accessing the HA web page directly. (Except you can deny Admin of course.)
You can remove named access to dashboards, but if they are curious that won't stop anything. You can still jump anywhere if you figure out how.

I created a small web page (actually a Progressive Web App) to give people access to a gate.

The PWA generates a UUID the first time it makes a call to HA via a webhook. I then add that UUID to my automation for a tiny bit of access control.

Webhooks don't return data, so my automation then posts the state to the same webserver so the PWA can show state (e.g. gate open/closed).

I did very extensive UX design on the app. :wink:

This is a great idea, I think I'll do something similar!

Great idea. I worked out a php page, with a password, using Claude last night. It will open and close the garage, with some feedback from HA about the garage door status. Claude even added some UI elements to make it look nice.

php:

<?php
// ── CONFIG ──────────────────────────────────────────────────────────────────
define('PAGE_PASSWORD',   'supersecretpassword');
define('HA_URL',          'https://home-assistant.mydomain.net/');
define('HA_TOKEN',        'home-assistant-long-lived-token');
define('HA_WEBHOOK_ID',   'very-long-random-char-string');
define('HA_DOOR_ENTITY',  'cover.smart_garage_door_opener');
define('STATE_DELAY_SEC', 2);
// ────────────────────────────────────────────────────────────────────────────

$message      = '';
$messageClass = '';
$doorState    = '';
$showCloseButton = false;
$showCloseConfirm = false;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {

    $action = isset($_POST['action']) ? $_POST['action'] : 'open';

    // ── CLOSE ACTION ─────────────────────────────────────────────────────────
    if ($action === 'close') {

        $serviceUrl = HA_URL . '/api/services/cover/close_cover';

        $ch = curl_init($serviceUrl);
        curl_setopt($ch, CURLOPT_POST,           true);
        curl_setopt($ch, CURLOPT_POSTFIELDS,     json_encode(['entity_id' => HA_DOOR_ENTITY]));
        curl_setopt($ch, CURLOPT_HTTPHEADER,     [
            'Authorization: Bearer ' . HA_TOKEN,
            'Content-Type: application/json',
        ]);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT,        5);
        curl_exec($ch);
        $closeStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        sleep(STATE_DELAY_SEC);

        $stateUrl = HA_URL . '/api/states/' . HA_DOOR_ENTITY;
        $ch = curl_init($stateUrl);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Authorization: Bearer ' . HA_TOKEN,
            'Content-Type: application/json',
        ]);
        curl_setopt($ch, CURLOPT_TIMEOUT, 5);
        $stateResult = curl_exec($ch);
        curl_close($ch);

        $stateData = json_decode($stateResult, true);
        $doorState = isset($stateData['state']) ? $stateData['state'] : 'unknown';

        $stateHu = [
            'open'    => 'NYITVA',
            'opening' => 'NYÍLIK',
            'closed'  => 'ZÁRVA',
            'closing' => 'ZÁRΓ“DIK',
            'unknown' => 'ISMERETLEN',
        ];
        $doorStateLabel = isset($stateHu[$doorState]) ? $stateHu[$doorState] : strtoupper($doorState);

        $message      = 'Parancs elkΓΌldve. Kapu Γ‘llapota: ' . htmlspecialchars($doorStateLabel);
        $messageClass = ($doorState === 'closed' || $doorState === 'closing') ? 'success' : 'warning';
        $showCloseConfirm = true;

    // ── OPEN ACTION ──────────────────────────────────────────────────────────
    } else {

        $submittedPassword = isset($_POST['password']) ? trim($_POST['password']) : '';

        if ($submittedPassword !== PAGE_PASSWORD) {

            $message      = 'Helytelen jelszΓ³.';
            $messageClass = 'error';

        } else {

            $webhookUrl = HA_URL . '/api/webhook/' . HA_WEBHOOK_ID;

            $ch = curl_init($webhookUrl);
            curl_setopt($ch, CURLOPT_POST,           true);
            curl_setopt($ch, CURLOPT_POSTFIELDS,     '{}');
            curl_setopt($ch, CURLOPT_HTTPHEADER,     ['Content-Type: application/json']);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_TIMEOUT,        5);
            $webhookResult = curl_exec($ch);
            $webhookStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);

            if ($webhookStatus >= 200 && $webhookStatus < 300) {

                sleep(STATE_DELAY_SEC);

                $stateUrl = HA_URL . '/api/states/' . HA_DOOR_ENTITY;
                $ch = curl_init($stateUrl);
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                curl_setopt($ch, CURLOPT_HTTPHEADER, [
                    'Authorization: Bearer ' . HA_TOKEN,
                    'Content-Type: application/json',
                ]);
                curl_setopt($ch, CURLOPT_TIMEOUT, 5);
                $stateResult = curl_exec($ch);
                curl_close($ch);

                $stateData = json_decode($stateResult, true);
                $doorState = isset($stateData['state']) ? $stateData['state'] : 'unknown';

                $stateHu = [
                    'open'    => 'NYITVA',
                    'opening' => 'NYÍLIK',
                    'closed'  => 'ZÁRVA',
                    'closing' => 'ZÁRΓ“DIK',
                    'unknown' => 'ISMERETLEN',
                ];
                $doorStateLabel = isset($stateHu[$doorState]) ? $stateHu[$doorState] : strtoupper($doorState);

                $message      = 'Parancs elkΓΌldve. Kapu Γ‘llapota: ' . htmlspecialchars($doorStateLabel);
                $messageClass = ($doorState === 'open' || $doorState === 'opening') ? 'success' : 'warning';

                // Show close button only if door is confirmed open/opening
                if ($doorState === 'open' || $doorState === 'opening') {
                    $showCloseButton = true;
                }

            } else {

                $message      = 'Nem sikerΓΌlt elΓ©rni a Home Assistantt (HTTP ' . $webhookStatus . ').';
                $messageClass = 'error';

            }
        }
    }
}
?>
<!DOCTYPE html>
<html lang="hu">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>GarΓ‘zskapu</title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }

        body {
            font-family: sans-serif;
            background: #1a1a2e;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
        }

        .card {
            background: #16213e;
            border: 1px solid #0f3460;
            border-radius: 12px;
            padding: 40px 32px;
            width: 100%;
            max-width: 380px;
            text-align: center;
        }

        .icon { font-size: 48px; margin-bottom: 16px; }

        h1 {
            color: #e0e0e0;
            font-size: 1.4rem;
            margin-bottom: 28px;
        }

        label {
            display: block;
            text-align: left;
            color: #a0a0b0;
            font-size: 0.85rem;
            margin-bottom: 6px;
        }

        input[type="password"] {
            width: 100%;
            padding: 12px 14px;
            border: 1px solid #0f3460;
            border-radius: 8px;
            background: #0f3460;
            color: #e0e0e0;
            font-size: 1rem;
            margin-bottom: 20px;
            outline: none;
        }

        input[type="password"]:focus {
            border-color: #e94560;
        }

        button {
            width: 100%;
            padding: 13px;
            border: none;
            border-radius: 8px;
            font-size: 1rem;
            font-weight: bold;
            cursor: pointer;
            letter-spacing: 0.5px;
        }

        .btn-open {
            background: #e94560;
            color: #fff;
        }

        .btn-open:hover { background: #c73652; }

        .btn-close {
            background: #ff9800;
            color: #fff;
            margin-top: 12px;
        }

        .btn-close:hover { background: #e68900; }

        .message {
            margin-top: 20px;
            padding: 12px 16px;
            border-radius: 8px;
            font-size: 0.95rem;
            font-weight: bold;
        }

        .success { background: #1a3a2a; color: #4caf50; border: 1px solid #4caf50; }
        .warning { background: #3a2a1a; color: #ff9800; border: 1px solid #ff9800; }
        .error   { background: #3a1a1a; color: #f44336; border: 1px solid #f44336; }
    </style>
</head>
<body>
<div class="card">
    <div class="icon">πŸš—</div>
    <h1>GarΓ‘zskapu</h1>

    <?php if ($showCloseButton): ?>

        <!-- Door confirmed open: show only the close button -->
        <div class="message <?php echo htmlspecialchars($messageClass); ?>">
            <?php echo $message; ?>
        </div>
        <form method="POST" action="">
            <input type="hidden" name="action" value="close">
            <button type="submit" class="btn-close" style="margin-top: 20px;">
                GarΓ‘zskapu zΓ‘rΓ‘sa
            </button>
        </form>

    <?php elseif ($showCloseConfirm): ?>

        <!-- Close command sent: show result, no further actions -->
        <div class="message <?php echo htmlspecialchars($messageClass); ?>">
            <?php echo $message; ?>
        </div>

    <?php else: ?>

        <!-- Default: show open form -->
        <form method="POST" action="">
            <input type="hidden" name="action" value="open">
            <label for="password">JelszΓ³</label>
            <input
                type="password"
                id="password"
                name="password"
                placeholder="Add meg a jelszΓ³t"
                autocomplete="current-password"
                required
            >
            <button type="submit" class="btn-open">GarΓ‘zskapu nyitΓ‘sa</button>
        </form>

        <?php if ($message !== ''): ?>
            <div class="message <?php echo htmlspecialchars($messageClass); ?>">
                <?php echo $message; ?>
            </div>
        <?php endif; ?>

    <?php endif; ?>

</div>
</body>
</html>

and here's the automation that goes with it.

alias: Garage Door Webhook Trigger
description: Opens garage door when webhook is called
triggers:
  - webhook_id: very-long-random-char-string
    allowed_methods:
      - POST
    local_only: false
    trigger: webhook
conditions: []
actions:
  - target:
      entity_id: cover.smart_garage_door_opener
    action: cover.open_cover
mode: single

Just make sure the devices and webhook_id match in both the php and the automation. Also, in most cases, the 'HA_URL' will need the port number - mine is behind a reverse proxy.

Put it a repo and pubilish the link.. seems nice addition to get translations on the page ;-p