# Deploy hooks
URL: https://synquic.tech/docs/guides/deploy-hooks.md

Give a project its own URL that redeploys it (or runs a job) when called, from CI, a registry, a cron service or a script. Covers token, HMAC and open authentication, service selection, responses and the delivery log.

import { Step, Steps } from 'fumadocs-ui/components/steps';

A **deploy hook** (the panel calls it an **incoming webhook**) is a URL that belongs to one project. When
something calls it with the right credential, Synq starts a deploy of that project, or runs one of your
[jobs](/docs/guides/jobs). Each hook has its own address and its own secret, and you can have several per
project (one for CI, one for a registry, and so on).

Use a deploy hook when the thing that knows "it is time to deploy" is not a Git push Synq already
receives:

- CI finished building and pushed a new image to `:main`.
- A pasted Dockerfile or Compose project has no repository to push to.
- A headless CMS published content and a static site must rebuild.
- A scheduler outside Synq should redeploy nightly.

For "deploy when I push to the repository", use [auto-deploy](/docs/guides/auto-deploy) instead.

## Create a hook

<Steps>

<Step>

### Open the Webhooks tab

Open the project, then **Source & Triggers → Webhooks**. The **Incoming webhooks** card ("Trigger a deploy
or a job from an external URL. Each webhook gets its own address and its own auth.") lists the project's
hooks. Press **New webhook**.

</Step>

<Step>

### Name it and choose the action

- **Name**: anything that tells you who calls it, for example "CI deploy hook".
- **Action**:
  - **Deploy**: "Redeploy this project or selected services when called."
  - **Run a job**: "Run one of your jobs when called." Pick the **Job**.
- **Services (optional)**, for **Deploy**: leave **Whole project** checked to redeploy everything, or tick
  the services that should redeploy together (for a Compose or multi-service project).

</Step>

<Step>

### Choose how callers authenticate

- **Token** (default): a secret bearer token. The caller sends `Authorization: Bearer <token>`.
- **HMAC signature**: the caller signs the request body with a shared secret and sends the signature in
  `X-Signature-256`. The secret itself never travels.
- **Open**: no auth. Anyone with the URL can trigger it. Use it only when the caller cannot send a header
  or sign, and treat the URL itself as a secret. A **Run a job** hook cannot be open.

</Step>

<Step>

### Save and copy the URL and secret

The hook appears in the list with its **Delivery URL** and, for Token and HMAC, its **Secret** (masked;
press **Reveal** to show it). Copy both into the tool that will call the hook. The row also says what the
hook does ("Redeploys this project", "Redeploys 1 selected service", "Runs the backup job") and shows an
auth badge: **Token**, **HMAC** or **Open**.

</Step>

</Steps>

The delivery URL looks like this on an install whose panel is at `https://app.example.com`:

```text
https://app.example.com/api/proxy/api/webhooks/incoming/<hook-id>
```

It uses the instance's public URL (`SYNQ_PUBLIC_URL`). An install without a public URL shows a local
address that outside services cannot reach; set a public URL first.

## Call a hook

A hook accepts `POST` with any body up to 5 MB. The body is ignored by the action; with HMAC it is what you
sign.

### Token

```bash
curl -X POST \
  -H "Authorization: Bearer $SYNQ_HOOK_TOKEN" \
  https://app.example.com/api/proxy/api/webhooks/incoming/<hook-id>
```

Only the `Authorization` header is read. A token in the query string (`?token=`) is not accepted, even
though the form's hint mentions it.

### HMAC signature

Sign the exact bytes you send with HMAC-SHA256 and the hook's secret, hex-encode the result, and send it as
`sha256=<hex>` in `X-Signature-256` (`X-Hub-Signature-256` and `X-Signature` are also read):

```bash
BODY='{"ref":"main","sha":"4f2c1e9"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SYNQ_HOOK_SECRET" | sed 's/^.* //')
curl -X POST \
  -H "Content-Type: application/json" \
  -H "X-Signature-256: sha256=$SIG" \
  --data "$BODY" \
  https://app.example.com/api/proxy/api/webhooks/incoming/<hook-id>
```

The `sha256=` prefix is required. Because the format matches GitHub's `X-Hub-Signature-256`, a GitHub
repository or organization webhook (content type `application/json`, secret set to the hook's secret) can
call an HMAC hook directly.

### From CI

A GitHub Actions step at the end of a workflow that pushed a new image:

```yaml
- name: Deploy on Synq
  run: |
    curl --fail -X POST \
      -H "Authorization: Bearer ${{ secrets.SYNQ_HOOK_TOKEN }}" \
      "${{ secrets.SYNQ_HOOK_URL }}"
```

GitLab CI:

```yaml
deploy:
  stage: deploy
  script:
    - 'curl --fail -X POST -H "Authorization: Bearer $SYNQ_HOOK_TOKEN" "$SYNQ_HOOK_URL"'
```

## Responses

| Status | Body | Meaning |
|---|---|---|
| `200` | `{"data":{"action":"deploy","ref":"<deployment-id>"}}` | Started. `ref` is the deployment ID (or the job run ID for a job hook). |
| `404` | `{"error":"Not found"}` | No such hook, the hook is disabled, or the credential is wrong. These all look the same on purpose, so the URL cannot be probed. |
| `502` | `{"error":"Action failed"}` | The credential was fine but the action could not start (for example a selected service no longer exists). The reason is in the delivery log. |
| `429` | | More than 120 webhook calls in a minute from the same IP address. |

## What a deploy hook deploys

- It deploys the project's current settings, like **Redeploy** on the **Deployments** tab: the latest
  commit of the deploy branch for a repository, the stored files for an upload, the configured image for
  an image project.
- It pulls registry images again instead of reusing the copy on the server, so a new push to the same tag
  (`:main`, `:latest`) is picked up.
- With services selected, exactly those services redeploy together, and the call fails (instead of
  deploying more) if one of them was deleted.
- The deploy runs with the permissions of the person who created the hook. If that person loses write
  access to the project, the hook stops working until someone with access edits or recreates it.

## Manage hooks

Each row in **Incoming webhooks** has:

- a switch to **enable or disable** the hook (a disabled hook answers `404`);
- **Rotate secret**: a new token or HMAC secret. The old one stops working immediately, so update the
  caller with the new value;
- **Delete**: the URL stops working immediately.

## Delivery log

Below the hooks, **Recent deliveries** ("GitHub pushes and custom hook calls received for this project")
lists every call newest first, with **Load more** for older ones. Each row shows the outcome:

| Outcome | Meaning |
|---|---|
| `dispatched` | The deploy or job started. |
| `skipped` | Nothing to do, for example a push to another branch or a commit that is already live. |
| `ignored` | An event that never deploys, such as a provider's ping or a non-push event. |
| `failed` | Something went wrong. The error is shown on the row. |

The same log includes push deliveries from GitHub and from [Git URL providers](/docs/guides/auto-deploy#gitlab-bitbucket-gitea-and-forgejo).
Calls rejected for a bad credential are recorded in the organization's audit log, not here.

## API

| Route | What it does |
|---|---|
| `GET /api/projects/:id/incoming-webhooks` | List a project's hooks. |
| `POST /api/projects/:id/incoming-webhooks` | Create one: `name`, `actionType` (`deploy` or `job`), `actionConfig` (`serviceIds`, or `jobKey`), `authMode` (`token`, `hmac` or `none`). The response includes the secret. |
| `PATCH /api/projects/:id/incoming-webhooks/:hookId` | Rename, enable or disable, or change the action or auth. |
| `POST /api/projects/:id/incoming-webhooks/:hookId/rotate` | New secret. |
| `DELETE /api/projects/:id/incoming-webhooks/:hookId` | Delete. |
| `GET /api/projects/:id/webhook-deliveries` | The project's delivery log. |
| `POST /api/webhooks/incoming/:hookId` | The public trigger (what the delivery URL points at). |

## Related

<Cards>
  <Card title="Auto-deploy on push" href="/docs/guides/auto-deploy" description="Deploy on every push from GitHub, GitLab, Bitbucket, Gitea or Forgejo." />
  <Card title="Deploy a Docker image" href="/docs/guides/deploy-docker-image" description="Run an image your CI publishes." />
  <Card title="Jobs" href="/docs/guides/jobs" description="The jobs a Run a job hook can start." />
</Cards>
