Skip to main content
A webhook trigger turns a button or drawer into a receiver: an HTTP endpoint you can register with GitHub, Stripe, Linear, Apify, or any other service that sends webhooks. This is the inverse of an HTTP API button. HTTP API buttons send requests; webhook triggers receive them. There are two webhook layers, and they are deliberately different:
  • Drawer webhook triggers (buttons drawer ... trigger webhook + buttons webhook listen) — n8n-style auth, the request body delivered into the workflow as ${inputs.webhook.body}, and an optional public Cloudflare URL. Use this when you need the payload.
  • Button webhook triggers (buttons trigger add ... --webhook + buttons serve) — a single shared token, no tunnel, and a body that is read then discarded (the press uses the trigger’s own args). Use this for a simple body-less ping on a port you already serve.

Drawer webhook trigger

buttons webhook listen runs the foreground dispatcher and (unless you pass --no-tunnel) brings up a Cloudflare tunnel so third-party services can reach the path. When a POST arrives the drawer is pressed with the request materialized as ${inputs.webhook.body} (plus .headers, .query, .method, .path, .received_at). Only the webhook kind is implemented for drawer triggers; other kinds error out. The tunnel requires cloudflared on PATH; run buttons webhook setup once to configure a stable named tunnel, or skip it and buttons webhook listen uses an ephemeral quick tunnel.

Flags

Default path: /<drawer-name>. A path is globally unique across drawers; reusing one another drawer owns is rejected. Any secret-bearing value accepts $ENV{VAR_NAME}, resolved at match time, so a committed drawer.json carries the auth shape but not the raw secret.

Auth example

Local testing

Dry-run a webhook drawer without the listener — --webhook-body synthesizes inputs.webhook:

Button webhook trigger

To fire a single button on a POST, attach a webhook trigger to it and run buttons serve (the local REST API server, which also runs the trigger engine unless you pass --no-triggers):
Differences from the drawer layer:
  • Auth is the single shared --token, checked against the X-Buttons-Token header or ?token= query param (constant-time). Omit --token for an open endpoint.
  • The body is read and discarded — v1 does not map it into args. The press uses the --arg values you configured on the trigger.
  • No Cloudflare tunnel. The path is mounted on whatever interface buttons serve binds (loopback by default), and the handler returns 202 Accepted immediately.
Manage triggers with buttons trigger list [button] and buttons trigger rm <button> <trigger-id>. The same buttons trigger add command also supports --cron --schedule "0 */6 * * *" and --watch --path ./file.