Start a goal from a webhook
A platform webhook is an HTTP endpoint on the Sails server. An external service POSTs JSON. Sails starts a background goal as you (the repository owner), not as an anonymous visitor.
Owner-only. Agents and write members cannot webhook add.
This is not a published *.api.sh handler. A site handler is fast bash on the public mount. A platform webhook is a multi-step agent run.
Done looks like
webhook add prints a URL. A POST with the right auth returns JSON like { "status": "accepted", "goal_id": "…" }. goal wait on that id finishes. webhook show NAME still shows the same URL after you change the prompt.
Worked example: CI failure → investigate
Job: your CI can POST JSON when a build fails. You want a goal in sites/app that reads the payload and suggests a fix.
webhook add ci-failure \
--path sites/app \
--prompt 'Investigate this CI failure and suggest a fix: '
--path scopes the goal the same way goal --path does. --verify defaults to bearer.
Save the printed bearer token (whk_…). It is shown once. Only a hash is stored.
The output also includes a URL:
https://your-server/sails/hooks/{uuid}
From CI (or your laptop):
curl -X POST "https://your-server/sails/hooks/{uuid}" \
-H "Authorization: Bearer whk_..." \
-H "Content-Type: application/json" \
-d @event.json
Accepted response includes goal_id. Then:
goal wait GOAL_ID
Inspect later with runs list --kind goal or goal log GOAL_ID.
Prompt placeholders
| Placeholder | Replaced with |
|---|---|
| `` | Full JSON body (pretty-printed) |
| `` | type field |
| `` | id field |
Change the prompt, keep the URL
Do not webhook rm + webhook add to fix a prompt or secret. That mints a new URL and breaks the provider until you update it there.
webhook update ci-failure --prompt 'CI failed. Summarize and open a fix plan: '
webhook list
webhook show ci-failure
Stripe or Svix instead of bearer
Stripe signs with Stripe-Signature. Svix-backed senders (Resend, Clerk, …) use svix-id, svix-timestamp, svix-signature. Store the signing secret first — see Store a secret.
secret set "whsec_..."
# prints: ssec_…
webhook add stripe-failures \
--path sites/app \
--verify stripe \
--secret ssec_… \
--events invoice.payment_failed,payment_intent.payment_failed \
--prompt 'Stripe event (). Summarize what failed.
'
webhook show stripe-failures
Paste that URL into the provider dashboard. Content type: application/json.
--events filters payload type. Other events still return HTTP 200 with { "status": "ignored" } so the provider does not retry forever.
--verify accepts bearer, stripe, or svix.
If Resend’s create-webhook returns a new signing_secret, secret set that value, then webhook update NAME --secret ssec_new…. Do not delete the webhook to rotate the secret.
Optional check:
webhook check-secret stripe-failures
Platform agent mail is separate
Inbound {uuid}@inbox.sails.app uses POST /sails/mail/resend and starts email.received as the agent. Do not webhook add for that. See Mail from an agent.
You can webhook add for your Resend account (bounces, your own receiving domain) with --verify svix.
Flags
webhook add NAME --prompt TEXT --path PATH [--verify bearer|stripe|svix] [--secret ssec_...] [--events TYPE,...]
webhook update NAME [--prompt TEXT] [--events TYPE,...] [--secret ssec_...]
webhook set-secret NAME --secret ssec_...
webhook check-secret NAME
webhook list
webhook show NAME
webhook rm NAME
| Command | What it does |
|---|---|
webhook add | Create. Bearer token shown once when --verify bearer |
webhook update | Change prompt, events, or secret without changing the URL |
webhook set-secret | Alias for webhook update --secret |
webhook check-secret | Signing-key fingerprint vs the provider whsec |
webhook show | URL, verify mode, prompt (not the bearer token) |
webhook rm | Delete. The URL dies. Last resort |
Failure modes
| What you see | What it means | What to do |
|---|---|---|
repository owner access required | Owner-only | Stop retrying. An owner runs webhook add, or Approves elevation in a Slack goal |
| Provider retries / 401 | Bad bearer or signing secret | Bearer: you must use the whk_… from create. Stripe/Svix: secret set the current whsec, then webhook update --secret |
| You lost the bearer token | Shown once; only a hash is stored | Owner creates a new webhook and updates the caller URL — or treat it as gone |
| Provider still hits an old URL | You webhook rm + add | webhook update next time. Fix the URL at the provider now |
{ "status": "ignored" } | --events filtered this type | Expected. Add the type to --events if you want a goal |
| Fast 200 but no agent work | You POSTed a published *.api.sh | Platform URL is /sails/hooks/{uuid}, not your site path |
| Agent inbox never fires | You pointed Resend at a platform webhook for agent mail | Agent mail is not webhook add — see Mail from an agent |
See Give an agent a job for goals that are not HTTP-triggered.