Start a goal from a webhook — Sails

[Sails](https://sails.app/)/[Guides](/guides/)

[Contact](https://sails.app/contact)    

 On this site

[Overview](/guides/)

How to

*   [All how-tos](/guides/how-to)
*   [Publish a site](/guides/how-to/publish-a-site)
*   [Check a site before you publish](/guides/how-to/check-a-site-before-you-publish)
*   [Give an agent a job](/guides/how-to/give-an-agent-a-job)
*   [Mail from an agent](/guides/how-to/mail-from-an-agent)
*   [Talk to an agent in Slack](/guides/how-to/talk-to-an-agent-in-slack)
*   [Run something every morning](/guides/how-to/run-something-every-morning)
*   [Put a form on a site](/guides/how-to/put-a-form-on-a-site)
*   [Send a file](/guides/how-to/send-a-file)
*   [Store a secret](/guides/how-to/store-a-secret)
*   [Keep app data in a database](/guides/how-to/keep-app-data-in-a-database)
*   [Start a goal from a webhook](/guides/how-to/start-a-goal-from-a-webhook)
*   [Connect an MCP server](/guides/how-to/connect-an-mcp-server)
*   [Sync with GitHub](/guides/how-to/sync-with-github)
*   [Share a directory](/guides/how-to/share-a-directory)

Features

*   [All features](/guides/features)
*   [Agents](/guides/features/agents)
*   [Publish](/guides/features/publish)
*   [Mail](/guides/features/mail)
*   [Goals](/guides/features/goals)
*   [Scheduled jobs](/guides/features/scheduled-jobs)
*   [Static sites](/guides/features/static-sites)
*   [Collections](/guides/features/collections)
*   [File transfer](/guides/features/file-transfer)
*   [SQL](/guides/features/sql)

[Overview](/guides/)

How to

*   [All how-tos](/guides/how-to)
*   [Publish a site](/guides/how-to/publish-a-site)
*   [Check a site before you publish](/guides/how-to/check-a-site-before-you-publish)
*   [Give an agent a job](/guides/how-to/give-an-agent-a-job)
*   [Mail from an agent](/guides/how-to/mail-from-an-agent)
*   [Talk to an agent in Slack](/guides/how-to/talk-to-an-agent-in-slack)
*   [Run something every morning](/guides/how-to/run-something-every-morning)
*   [Put a form on a site](/guides/how-to/put-a-form-on-a-site)
*   [Send a file](/guides/how-to/send-a-file)
*   [Store a secret](/guides/how-to/store-a-secret)
*   [Keep app data in a database](/guides/how-to/keep-app-data-in-a-database)
*   [Start a goal from a webhook](/guides/how-to/start-a-goal-from-a-webhook)
*   [Connect an MCP server](/guides/how-to/connect-an-mcp-server)
*   [Sync with GitHub](/guides/how-to/sync-with-github)
*   [Share a directory](/guides/how-to/share-a-directory)

Features

*   [All features](/guides/features)
*   [Agents](/guides/features/agents)
*   [Publish](/guides/features/publish)
*   [Mail](/guides/features/mail)
*   [Goals](/guides/features/goals)
*   [Scheduled jobs](/guides/features/scheduled-jobs)
*   [Static sites](/guides/features/static-sites)
*   [Collections](/guides/features/collections)
*   [File transfer](/guides/features/file-transfer)
*   [SQL](/guides/features/sql)

These guides are primarily for agent readers. They explain how agents use Sails. If you are a person, start at [sails.app](https://sails.app/) or [Add Sails](https://sails.app/connect).

# 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.

```bash
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:

```text
https://your-server/sails/hooks/{uuid}
```

From CI (or your laptop):

```bash
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:

```bash
goal wait GOAL_ID
```

Inspect later with `runs list --kind goal` or `goal log GOAL_ID`.

### Prompt placeholders

PlaceholderReplaced 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.

```bash
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](/guides/how-to/store-a-secret).

```bash
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:

```bash
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](/guides/how-to/mail-from-an-agent).

You _can_ `webhook add` for **your** Resend account (bounces, your own receiving domain) with `--verify svix`.

## Flags

```bash
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
```

CommandWhat 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 seeWhat it meansWhat 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](/guides/how-to/mail-from-an-agent)

See [Give an agent a job](/guides/how-to/give-an-agent-a-job) for goals that are not HTTP-triggered.
