Skip to content

Webhook triggers

A webhook lets an external service post a signed payload and have CommandLatch fire a pinned action on the paired device.

Each webhook is a narrow, single-purpose credential:

  • One device (the action fires there and only there).
  • One action (lock, lock_sleep, keep_awake_start, etc.).
  • One signing secret (HMAC-SHA256 over the request body).
  • One rate limit (per minute, sliding window).
  • Revocable independently from the device’s pairing.

The request body is optional. When supplied, its payload field merges into the webhook’s stored payload defaults, so callers can pass per-event fields like delay_secs without rewriting the webhook config.

In the web dashboard, expand Webhooks under the target device, fill in a name + action, and submit. The page shows the webhook URL, the signing secret, and a ready-to-use curl example.

Copy both the URL and the secret right away — the secret is shown exactly once. If you lose it, delete the webhook and create a new one.

Stripe-style signature header: X-Signature: t=<unix_seconds>,v1=<hex_hmac>. HMAC input is <timestamp> "." <raw_body>, key is the signing secret.

Terminal window
SECRET='<paste the signing secret>'
URL='<paste the webhook URL>'
TS=$(date +%s)
BODY='{"payload":{"delay_secs":60}}' # optional; '' is fine too
# -binary | od ... reads the raw HMAC bytes and hex-encodes them, which
# avoids relying on openssl's text output format (it differs between
# LibreSSL and OpenSSL 3.x — see Troubleshooting below).
SIG=$(printf "%s.%s" "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -binary | od -An -tx1 | tr -d ' \n')
curl -X POST "$URL" \
-H "X-Signature: t=$TS,v1=$SIG" \
-H "Content-Type: application/json" \
--data-raw "$BODY"

Successful trigger:

{ "command_id": "…", "expires_at": "2026-05-14T…Z" }

The Mac picks up the command within a couple of seconds.

header: X-Signature: t=<unix_seconds>,v1=<hex_hmac_sha256>
input: "<unix_seconds>.<raw_request_body_bytes>"
key: webhook signing secret (32 random bytes, base64-encoded)
digest: HMAC-SHA256, hex-encoded lowercase

The server rejects anything outside ±5 minutes of its own clock to bound replay windows. Empty bodies are allowed — the HMAC then signs "<ts>.", which still proves possession of the secret.

The signing secret is used only for HMAC verification and is never returned through the API — not even to you.

Each webhook is limited to 12 requests per minute by default. Every request counts — including rejected ones — so if a caller hits the limit, it stays limited until the window resets.

Contact support if a legitimate automation needs a higher limit.

StatusBodyMeaning
201{"command_id","expires_at"}Queued for the agent.
401{"error":"invalid_webhook"}Unknown id or no signature.
401{"error":"bad_signature"}HMAC mismatch.
401{"error":"stale_timestamp"}Skew > 5 minutes.
403{"error":"webhook_disabled"}Owner toggled it off.
403{"error":"remote_disabled"}Device has remote commands off.
429{"error":"rate_limited"}Window cap exceeded.
405{"error":"method_not_allowed"}Use POST (or OPTIONS for CORS).
500{"error":"…"}Database error; retry.

Worked example: Zapier “When task is done, lock my Mac”

Section titled “Worked example: Zapier “When task is done, lock my Mac””
  1. Create a webhook on the dashboard with action lock_sleep and default payload {"delay_secs": 300}.
  2. Copy the URL + secret.
  3. In Zapier, add a Code step that computes the signature (the snippet above maps directly), then a Webhooks → POST step that sends the body with X-Signature set from the previous step’s output.
  4. The next time Zapier fires the trigger, the Mac displays a banner (if the webhook’s action were send_notification) or starts a 5-minute pending lock. Cancel from the menu bar, the dashboard, or commandlatch done --cancel.
  • {"error":"bad_signature"} with the correct secret. The HMAC input must be exactly <timestamp>.<raw_body> — no trailing newline, no re-serialized JSON. If your tool appends a newline (e.g. echo without -n), the digest won’t match.
  • {"error":"bad_signature"} using openssl dgst with awk. The output format of openssl dgst -hmac differs between LibreSSL and OpenSSL 3.x. The example above avoids this by piping through -binary | od -An -tx1 | tr -d ' \n', which produces consistent output on both. If you’re adapting another snippet, confirm your SIG variable contains a 64-character hex string.
  • {"error":"stale_timestamp"}. The request timestamp is more than 5 minutes from the server’s clock. Ensure your system clock is synchronized (NTP), or compute the timestamp immediately before sending the request.
  • Empty body but signature mismatch. Use BODY='' explicitly. If BODY is unset, printf may produce an unexpected result. The HMAC input and the request body must always match exactly.
  • Lost signing secret. The secret is shown only once in the dashboard and cannot be retrieved later. If you lose it, delete the webhook and create a new one.
  • Webhook fires the wrong action. The action is set at creation and cannot be changed. Delete the webhook and create a new one — the URL will change, which ensures any upstream callers are updated deliberately.