---
title: Webhooks
description: Trigger stack actions from CI/CD pipelines via HTTP webhooks with HMAC signature authentication.
---

<Note>
  Managing webhooks is admin-only.
</Note>

Sencho webhooks let external systems trigger stack actions over HTTP. The typical use case: your CI pipeline builds a new image, then calls a Sencho webhook to deploy the updated stack, with no manual intervention required.

## How it works

1. You create a webhook in **Settings → Automation → Webhooks**, targeting a specific stack and action.
2. Sencho generates a unique secret for HMAC-SHA256 signature validation.
3. Your CI/CD system sends a `POST` request to the trigger URL with the correct signature.
4. Sencho validates the signature and executes the action asynchronously.

## Creating a webhook

Open **Settings → Automation → Webhooks** and click **Create webhook**. The page is part of the global settings and is reachable while the **Local** node is active in the node switcher; webhooks created from this page execute against the local Sencho instance.

<Frame>
  <img src="/images/webhooks/webhooks-create-form.png" alt="New webhook form with Name, Stack, Node, and Action fields. The Action dropdown is open showing Deploy (down + up), Restart, Stop, Start, Pull and Update, and Git source sync." />
</Frame>

| Field | Description |
|-------|-------------|
| **Name** | A display name shown in the configured-webhooks list and execution history. |
| **Stack** | The target stack to act on. Picked from the stacks on the active node. |
| **Node** | Read-only. Reflects the currently active node; webhook execution is pinned to that node. |
| **Action** | One of the actions listed below. |

After you click **Create**, the new webhook's secret appears once in a green callout titled **Webhook created. Copy your secret now.** Copy it before dismissing the callout; once you navigate away, it cannot be retrieved. The configured-webhooks list always shows the secret in masked form (`********` plus the last four characters).

<Frame>
  <img src="/images/webhooks/webhooks-secret-reveal.png" alt="Green success callout after creating a webhook, showing the full secret in a monospace block and a Copy button next to a Dismiss button. Below the callout, the configured-webhooks list shows the new webhook card with its trigger URL, masked secret, and Recent executions expanded to show one successful deploy entry with its timestamp and duration." />
</Frame>

### Available actions

| Action | What it does |
|--------|-------------|
| **Deploy (down + up)** | Stops and redeploys the entire stack. |
| **Restart** | Restarts the stack's running containers. |
| **Stop** | Stops the stack. |
| **Start** | Starts a stopped stack. |
| **Pull & Update** | Pulls the latest images and recreates changed containers. |
| **Git source sync** | Pulls the latest commit from the stack's Git source, then deploys. Requires a [Git source](/features/git-sources) attached to the stack. |

## Managing webhooks

Each configured webhook renders as a card showing the webhook name, an **action** badge, a **stack** badge, a **node** badge, an enable toggle, and a delete button. Below the header are a labeled **Trigger URL** row with a copy button, a labeled **Secret** row showing the masked secret, and a **Recent executions** disclosure that expands inline.

The masthead carries `SCOPE` (`global`), `WEBHOOKS` (total), and `ENABLED` (count).

<Frame>
  <img src="/images/webhooks/webhooks-settings.png" alt="Configured webhooks section with two webhook cards, each showing its name, action badge, stack badge, node badge, an On toggle, and a delete icon. The first card's Recent executions is collapsed; the second card's is expanded, showing one successful deploy entry with its timestamp and duration. The masthead at the top shows SCOPE global, WEBHOOKS 2, and ENABLED 2." />
</Frame>

From a webhook card you can:

- Toggle the webhook on or off with the **On / Off** switch.
- Delete the webhook with the trash icon.
- Copy the trigger URL to the clipboard with the copy button.
- Expand **Recent executions** to view the inline history.

The card has no in-place edit affordance: to change the name, stack, or action, delete the webhook and create a new one. The signing secret is bound to a single webhook for its lifetime; deleting and recreating rotates it.

## Triggering a webhook

Send a `POST` request to the trigger URL shown on the webhook card. Include an HMAC-SHA256 signature in the `X-Webhook-Signature` header:

```
POST /api/webhooks/{id}/trigger
Content-Type: application/json
X-Webhook-Signature: sha256={hmac}
```

The signature is computed as `HMAC-SHA256(raw_request_body, webhook_secret)` over the exact bytes of the request body (UTF-8 if you send JSON) and sent with a `sha256=` prefix. Sencho uses a constant-time comparison.

### Example with curl

```bash
SECRET="your-webhook-secret"
BODY='{}'
SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)

curl -X POST https://your-sencho.example.com/api/webhooks/3/trigger \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: sha256=$SIGNATURE" \
  -d "$BODY"
```

### Overriding the action

By default, the webhook executes the action configured at creation time. You can override it per request by including an `action` field in the body:

```json
{ "action": "restart" }
```

Valid override values are: `deploy`, `restart`, `stop`, `start`, `pull`, `git-pull`. An unknown value is rejected with `400 Bad Request` before execution is queued.

### Concurrency and the per-stack lock

Deploy, Restart, Stop, Start, and Pull & Update share the same per-stack lock as manual actions started from the UI. If the stack already has one of those operations in flight when the webhook fires, Sencho still returns `202 Accepted`, but the action itself is skipped rather than queued behind the running one. The skip shows up in **Recent executions** as a failed entry with a message like `Skipped "stack-name": another operation (deploy) is already in progress.` Retry the trigger once the in-flight operation finishes.

Git source sync is the exception: it waits for an in-progress pull or apply on the same stack to finish rather than skipping (see [Local edits vs Git](/features/git-sources#local-edits-vs-git)).

### Responses

| Status | Body | Meaning |
|--------|------|---------|
| `202 Accepted` | `{ "message": "Webhook accepted", "action": "deploy" }` | Signature is valid; the action is now running asynchronously. A 202 means accepted, not finished. |
| `400 Bad Request` | `{ "error": "action must be one of: deploy, restart, stop, start, pull, git-pull" }` | The request authenticated successfully but the body's `action` override was not in the allowlist. |
| `404 Not Found` | `{ "error": "Webhook not found or signature invalid" }` | The webhook id is unknown, the webhook is disabled, the signature header is missing, the request body was empty, or the signature did not match. Sencho returns the same response for every unauthenticated case so callers cannot probe ids or licence state. |
| `429 Too Many Requests` | `{ "error": "Too many webhook triggers. Please try again shortly." }` | The trigger endpoint allows 500 requests per minute per source IP. CI/CD callers behind shared NAT or datacenter egress may share that budget. |

## CI/CD integration examples

### GitHub Actions

```yaml
- name: Deploy via Sencho
  run: |
    BODY='{}'
    SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "${{ secrets.SENCHO_WEBHOOK_SECRET }}" | cut -d' ' -f2)
    curl -X POST "${{ secrets.SENCHO_URL }}/api/webhooks/${{ secrets.SENCHO_WEBHOOK_ID }}/trigger" \
      -H "Content-Type: application/json" \
      -H "X-Webhook-Signature: sha256=$SIGNATURE" \
      -d "$BODY"
```

### GitLab CI

```yaml
deploy:
  stage: deploy
  script:
    - BODY='{}'
    - SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SENCHO_WEBHOOK_SECRET" | cut -d' ' -f2)
    - 'curl -X POST "$SENCHO_URL/api/webhooks/$SENCHO_WEBHOOK_ID/trigger"
        -H "Content-Type: application/json"
        -H "X-Webhook-Signature: sha256=$SIGNATURE"
        -d "$BODY"'
```

## Execution history

Click **Recent executions** on any webhook card to expand its inline history. Each row shows:

- A **status** icon (green check for success, red X for failure).
- The **action** that ran (the configured action, or the override if one was supplied).
- A localized **timestamp**.
- The **duration** in seconds when available.
- A truncated **error** message on failures; hover the row to see the full message in a tooltip.

Sencho retains the last 100 executions per webhook and surfaces the 20 most recent in the inline view.

## Security

- Webhook secrets are returned only once, at the moment of creation. Every subsequent view of the webhook (in the list, in the API, anywhere) shows the masked form `********` plus the last four characters.
- Trigger endpoints are public in the sense that they do not require a Sencho session cookie, but every request must carry a valid HMAC-SHA256 signature; signatures are compared with a constant-time check.
- Each webhook targets a single stack and runs a single action, optionally narrowed by the supplied override. There is no path to execute arbitrary commands through a webhook.
- Webhook execution is pinned to the node selected at creation time.

## Troubleshooting

<AccordionGroup>
  <Accordion title="I don't see Webhooks in Settings.">
    Webhooks are managed on the local node. If the node switcher in the top-left shows a remote node, switch to **Local** to reveal the page.
  </Accordion>

  <Accordion title="My trigger returns 404 'Webhook not found or signature invalid'.">
    The trigger endpoint returns the same 404 for every unauthenticated case, so the response alone will not tell you which check failed. Work through this checklist:

    - The id in the URL is wrong. Copy the trigger URL straight from the webhook card in **Settings → Automation → Webhooks**.
    - The webhook's enable toggle is off. Check the **On / Off** switch on the card.
    - The request omitted the `X-Webhook-Signature` header. Confirm the full value, including the `sha256=` prefix; the header value is `sha256={hex}`.
    - The request had no body. Send at least `{}` and sign that exact byte sequence.
    - The signature was computed over different bytes from what the request carried. Sign the exact string you send. Common offenders: the CI step JSON-encodes the body before signing but pipes a different body to curl, or the shell adds a trailing newline (use `echo -n` rather than `echo`).
    - The webhook secret used to sign does not match the secret stored in Sencho. The secret is shown once at creation; if you lost it, delete the webhook and create a new one.
  </Accordion>

  <Accordion title="The action returns 202 but the stack does not change.">
    A 202 means the action was accepted, not that it produced a visible change. **Pull & Update** is a no-op if the registry has no newer image for any service in the stack; **Start** is a no-op on an already-running stack. Check the stack's **Activity** sheet to confirm what actually ran, and the webhook's **Recent executions** for the action's status and duration.
  </Accordion>

  <Accordion title="Recent executions shows 'Skipped ... another operation is already in progress'.">
    The webhook's action shares the stack's per-stack lock with manual actions from the UI. The webhook fired while a manual deploy, update, restart, stop, start, rollback, or backup on the same stack was still running, so Sencho skipped the webhook's action rather than queuing it behind the running one. Retry the trigger once the in-flight operation finishes. Git source sync does not hit this path; it waits for the in-progress operation instead of skipping.
  </Accordion>

  <Accordion title="My trigger returns 429 'Too many webhook triggers'.">
    The trigger endpoint is rate limited to 500 requests per minute per source IP. CI/CD runners behind shared NAT or a datacenter egress pool all share that budget. Either space the calls out, send them from a less crowded egress, or split the work across multiple webhooks if a single workflow fires hundreds of triggers in close succession.
  </Accordion>

  <Accordion title="I cannot create a Git source sync webhook for this stack.">
    The **Git source sync** action can only be selected for a stack that already has a [Git source](/features/git-sources) attached. If you try to create one without it, the form returns a validation error. Attach a Git source to the stack in its Compose editor, then create the webhook.
  </Accordion>
</AccordionGroup>
