# Webhook trigger

The Webhook trigger exposes a public HTTP endpoint that runs your workflow whenever an external system POSTs (or GETs, PUTs, PATCHes, DELETEs) to it. Use it for callbacks from Stripe, GitHub, Typeform, your own backend — anything that can hit a URL.

## Add it to a workflow

1. Drop a **Webhook** node from the node palette onto the canvas.
2. Open the config panel and set:
   - **Method** — `GET`, `POST`, `PUT`, `PATCH`, or `DELETE` (default `POST`).
   - **Path** — any suffix (e.g. `/stripe`). Leading slash optional.
   - **Auth** — see below.
   - **Response mode** — `lastNode` (sync, default) or `asyncAck` (fire-and-forget).
3. **Save** the workflow and make sure it is active (Live toggle in the Production space). Inactive agents return `404` — the endpoint only goes live once the workflow is active.

## The public URL

Once active, your endpoint is:

```
https://<your-host>/api/v1/wh/<workflow_id>/<path>
```

`<workflow_id>` can be either the numeric `id` or the workflow's `public_id`. Example:

```
https://app.systalink.io/api/v1/wh/42/stripe
https://app.systalink.io/api/v1/wh/wf_8ab3c1.../stripe
```

The router matches on both **path** and **method**, so you can wire several Webhook nodes into the same workflow (e.g. `POST /stripe` and `POST /github`).

## Auth

A Webhook trigger **must be authenticated** — `authType: none` is rejected at save with `400 "The Webhook trigger must be authenticated…"`, and the node also requires a **path**. Pick one of:

| `authType`        | What the caller sends                                  |
| ------------------ | ------------------------------------------------------ |
| `bearerSecret`    | `Authorization: Bearer <token>` (raw token also OK).  |
| `apiKeySecret`    | Custom header (default `X-API-Key: <token>`).         |
| `basic`           | `Authorization: Basic <base64 user:pass>`.            |
| `hmac`            | Signed request body (shared-secret HMAC signature).    |

Secrets are encrypted at rest. The server fails closed: if auth is required but no secret is configured, every call gets `401`.

## Payload the workflow receives

The trigger normalizes the request into a single object available to downstream nodes:

```json
{
  "trigger_kind": "webhook",
  "webhook": {
    "body":    { ... },          // parsed JSON, or raw string if not JSON
    "headers": { "host": "...", "content-type": "..." },
    "query":   { "foo": "bar" },
    "method":  "POST",
    "path":    "/stripe",
    "raw_body": "{\n  \"event\": \"ping\"\n}",
    "raw_body_base64": "ewogICJldmVudCI6ICJwaW5nIgp9",
    "raw_body_sha256": "..."
  },
  "event":       { ... },        // trigger-agnostic alias of `webhook`
  "input":       { ... },        // alias of webhook.body
  "last_output": { ... }         // also alias of webhook.body
}
```

In any downstream node, template directly into prompts or fields. Two equivalent prefixes:

- `{event.body}` / `{webhook.body}` — full body
- `{event.body.customer.email}` — drill into JSON
- `{event.headers.x-github-event}` — read a header
- `{event.query.token}` — read a query-string param
- `{event.raw_body}` — exact UTF-8 body, including whitespace
- `{event.raw_body_base64}` — exact body bytes as base64 for binary/HMAC flows
- `{event.raw_body_sha256}` — SHA-256 hash of the exact request body
- `{last_output}` — body again (handy for one-hop Webhook → Agent)

For provider signatures, point an HMAC node at `event.raw_body` for normal JSON/text webhooks. For binary or non-UTF-8 payloads, use `event.raw_body_base64` and set the HMAC input encoding to **Base64 bytes**.

> `event.*` is the recommended form because it works identically for **schedule** and **event listener** triggers too. Prefer it for portability.

## Response semantics

- **`lastNode`** (default) — the HTTP call blocks up to **30 seconds** for the run to finish, then returns its last output as JSON (or whatever a downstream `Respond to Webhook` node sets). If the run is still going at 30s, you get `202 Accepted` with `{run_id, status: "in_progress"}` — fetch the final result from the run-poll endpoint below.
- **`asyncAck`** — return `202` immediately with `{run_id, status: "pending"}`. The workflow runs in the background; fetch the result from the run-poll endpoint below. If you include a `Respond to Webhook` node downstream, the router automatically waits for it even in async mode.

## Fetching the result (run poll)

Whenever a call returns `202` (async, or sync that exceeded 30s), poll the run with the **same credentials as the webhook itself** (the bearer / API-key / basic / HMAC auth you configured on the node):

```
GET /api/v1/wh/{workflow_id}/runs/{run_id}
```

```json
{
  "run_id":           "run_abc123",
  "status":           "completed",
  "result":           { ... },
  "webhook_response": { ... },
  "error":            null,
  "finished_at":      "..."
}
```

`status` is one of `pending`, `running`, `completed`, `failed`. Poll until it is terminal (`completed` or `failed`). Run results stay pollable for **30 days**, after which this endpoint returns `404`.

## Example: curl

```bash
curl -X POST https://app.systalink.io/api/v1/wh/42/stripe \
  -H 'Authorization: Bearer sk_test_abc123' \
  -H 'Content-Type: application/json' \
  -d '{"event":"invoice.paid","amount":4200}'
```

## Troubleshooting

| Symptom                                | Cause                                                                            |
| -------------------------------------- | -------------------------------------------------------------------------------- |
| `404 Not Found`                       | Workflow id wrong, workflow not active, or no Webhook node matches that path+method. |
| `401 Invalid webhook credentials`     | Missing/wrong `Authorization` (or API-key) header, or node requires auth but has no secret set. |
| `500`                                  | Workflow ran and **failed** — body contains `{error: "..."}`. Check the Runs tab. |
| `202 in_progress`                     | Sync mode timed out at 30s. Poll `GET /api/v1/wh/{workflow_id}/runs/{run_id}`. |

> The endpoint is intentionally public — auth is per-node, not per-route. Always set `bearerSecret` or `apiKeySecret` for anything that triggers real work.

---

*Source: https://agentbuilder.systalink.sn/docs/trigger-webhook — human documentation.*
*Other language: [/docs-md/fr/trigger-webhook.md](/docs-md/fr/trigger-webhook.md).*
*Machine-readable index: [/llms.txt](/llms.txt).*
