Browse the docs

Webhook

Runs the macro when something sends an HTTP GET or POST to /hooks/<route>, with a real reply, a URL key for senders that cannot set headers, and optional signature checks.

A request arrives at route

Keyboard Maestro: Remote, WebServer

Details #

A request to /hooks/<route> starts every macro with a webhook trigger on that route (not case sensitive). The route may have slashes, such as github/push.

Methods. methods lists what the route takes: ["POST"] (the default, as before), ["GET"] or both. A request with another method gets 405 and an Allow header, and the attempt shows in Activity › Not fired with the reason "method". HEAD answers like GET without running anything, and OPTIONS answers the allowed methods.

Inputs. The run gets one webhook input, also readable as {{webhook.query.n}}, {{webhook.json.status}}, {{webhook.form.name}} and so on:

FieldWhat it holds
method, route, remoteGET or POST, the route, the sender's address
queryQuery items; a repeated name is a list
headersRequest headers with lower-case names. Authorization, Cookie, the key header and the signature header are never included
bodyThe raw text, or a file path for a binary body
jsonThe parsed JSON body (Content-Type: application/json). Invalid JSON is a 400 and nothing runs
formThe fields of a url-encoded or multipart form. An uploaded file is saved to a temporary folder and its field holds the path
textThe body of a text/* request
contentTypeThe content type without parameters

Keys of a JSON object body and every query item are also plain inputs (a query item wins over a body key of the same name), and body is the raw text, as before. {{triggerValue}} is the JSON or text body of a POST, the route for a GET, or the value query item when there is one (Keyboard Maestro's Remote trigger sends ?value=).

Auth. auth says who may call:

  • token (the default): Authorization: Bearer <local API token>, on this Mac only.

  • key: a random key made for the route and kept in the Keychain, never in the macro file. Send it as ?key= or the X-Switchboard-Key header. This is for senders that cannot set an Authorization header: a bookmark, an iOS Shortcut, a Stream Deck URL button. The editor's Copy URL button builds the full URL.

  • signature: an HMAC of the raw body, as GitHub, Stripe and Linear send. Set signature to {header, secretRef, scheme}; secretRef names the shared secret stored in the Keychain, scheme is sha256 (default) or sha1.

  • none: no auth, and only with reach: local.

A failed request gets 401 and never runs. Five failures in a minute from one address lock that address out for ten minutes (Settings › Webhooks lists and clears them).

Reach. local (the default) is this Mac only: the Unix socket and 127.0.0.1:48080. network also opens a second listener on port 48081 for your local network, advertised over Bonjour as _switchboard._tcp. It serves /hooks/* and nothing else, and needs key or signature auth; the token is never accepted there. macOS asks about Local Network access the first time. internet relays https://hooks.switchboardmac.com/<account>/<route> to the Mac through Switchboard's server (needs an account).

Reply. accepted (the default) answers 202 with {"runIds": [...]} at once. wait holds the request open until a Reply to webhook step replies, or the macro finishes (200 with its last result as text, or 500 with {error, step} if it failed), or replyTimeout seconds pass (default 30, at most 120; the answer is 504 and the run keeps going). If several macros share a route in wait mode, the first to reply wins and the rest run as if accepted.

While this Mac is away. An internet route normally needs the Mac awake and connected. For a route that answers accepted and checks a key or a signature, the server keeps a request that arrives while the Mac is asleep or offline: it checks what it can (the route, the method and, for key, the key), stores the request for up to 72 hours, answers the sender 202 with {"held": true, "id": …} and the header X-Switchboard-Held: 1, and hands it over when the Mac reconnects. whenAway chooses: hold (the default) or refuse, which answers 503 as before. A route that waits for a reply (reply: wait) can't be held, because its caller needs the answer now.

  • Order and age. Held requests arrive oldest first, one at a time. A request that comes while the backlog is being delivered doesn't wait behind it, so order is kept among held requests only. The run's webhook.held input is {receivedAt, waited} (Unix seconds, and seconds waited); it is absent for a live request. Replays skip the per-route rate limit; the macro's run.concurrent setting and the storm guard still apply.

  • Skip if it waited too long. maxDelay (seconds) skips a held request that waited longer: nothing runs, the sender's request is already answered, and Activity › Not fired shows the reason late. Use it for a doorbell or a "start the build" ping, where an hour-old event is useless.

  • At least once, run exactly once. If the connection drops after the Mac ran a held request but before the server heard, the same request comes again. The Mac keeps the ids it has handled for seven days (relay-seen.json) and answers the repeat 200 {"duplicate": true} without running anything; Not fired shows duplicate.

  • Keys and signatures are checked again. A signature route is checked on this Mac when the request is delivered, as a live one is. A request held with a key you have since rotated fails there (401) and shows in Not fired as auth.

  • Limits. At most 500 requests and 50 MB are held per account (100 for each signature route). When it is full the server answers 503 hold_full so a sender that retries tries again later. Settings › Webhooks shows what is held and Discard… deletes it.

Limits. The body can be up to 10 MB (413 above). A route takes 30 requests a minute (Settings › Webhooks), then answers 429 with Retry-After. A macro set to ignore overlapping runs answers 409 while it is busy.

Secure input does not affect this trigger.

Parameters #

ParameterTypeRequiredValuesWhat it does
routetextYesThe path after /hooks/. Letters, digits, dashes and slashes.
authone oftoken key signature nonetoken (default), key, signature or none.
maxDelaynumberSeconds. A held request that waited longer than this is skipped (Not fired: late).
methodslist ofGET POST["POST"] (default), ["GET"] or ["GET","POST"].
reachone oflocal network internetlocal (default), network or internet.
replyone ofaccepted waitaccepted (default) or wait.
replyTimeoutnumberSeconds to wait for a reply in wait mode. Default 30, at most 120.
signaturerecordheader scheme secretRef{header, secretRef, scheme} for auth: signature.
whenAwayone ofhold refusehold (default) or refuse. What the server does with a request for an internet route while this Mac is away.

Every trigger also takes Limits: a cooldown, the first time each day, every nth time, twice within seconds, a chance, only while you are at the Mac, on one Mac, or in a State. See Limits on any trigger.

Examples #

Receive a webhook
When
  1. A webhook is called at /build-done
Do
  1. Show “Hook {{triggerValue}} fired”
Open in Switchboard to try it
Answer a GET from a bookmark or an iPhone Shortcut
When
  1. A webhook is called at /shortcuts/lights
Do
  1. Show “Lights on”
  2. Reply to the webhook with 200
Open in Switchboard to try it
curl -X POST -H "Authorization: Bearer $TOKEN" -d '{"status":"passed"}' -H 'Content-Type: application/json' http://127.0.0.1:48080/hooks/build-done
curl "http://127.0.0.1:48080/hooks/shortcuts/lights?state=on&key=$KEY"

Gotchas #

  • A request for a route nobody listens to gets a 404 once you are signed in, and a 401 otherwise, so a stranger learns nothing about your routes.

  • Declare inputs on the macro to use body keys as variables.

  • A shared macro carries no key: the route's key is made on the Mac that adds the trigger.

  • Not fired reasons: disabled, groupOff, scope, paused, conditionFalse, method, auth, rateLimited, bodyTooLarge, late, duplicate.

See also #

URL, Reply to webhook, Triggers