---
title: "Deploy hooks"
description: "Deploy a service from your CI pipeline with one POST request."
canonical_url: "https://harblet.com/docs/deploy/deploy-hooks"
---
# Deploy hooks

> Deploy a service from your CI pipeline with one POST request.

Every image and Git service has a secret **deploy hook** URL. A `POST` request to it deploys the service, just like the **Deploy** button. Call it at the end of your CI pipeline, after tests and linting pass.

- A **Git service** builds the newest commit of its branch.
- An **image service** pulls its tag again. Push the new image under the same tag first.

The hook deploys the service's saved settings and environment variables, including changes that haven't been deployed yet.

## Get the URL

Open the service's **Settings** tab. The **Deploy hook** card shows the URL and a ready-made command:

```bash
curl --fail -X POST https://harblet.example.com/api/hooks/2f1c9e4a-7b3d-4e8f-9a61-0c5d8e7f3b12
```

<warning>

Anyone with the URL can deploy the service. Store it as a secret in your CI system, not in your repository.

</warning>

## Call it from your pipeline

<tabs>
<tabs-item icon="i-simple-icons-githubactions" label="GitHub Actions">

Add the URL as a repository secret named `HARBLET_DEPLOY_HOOK`, then add a step at the end of your workflow:

```yaml [.github/workflows/deploy.yml]
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test
      - name: Deploy to Harblet
        run: curl --fail -X POST "${{ secrets.HARBLET_DEPLOY_HOOK }}"
```

</tabs-item>

<tabs-item icon="i-simple-icons-gitlab" label="GitLab CI">

Add the URL as a masked CI/CD variable named `HARBLET_DEPLOY_HOOK`, then add a job:

```yaml [.gitlab-ci.yml]
deploy:
  stage: deploy
  image: curlimages/curl
  script:
    - curl --fail -X POST "$HARBLET_DEPLOY_HOOK"
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
```

</tabs-item>
</tabs>

## Responses

<table>
<thead>
  <tr>
    <th>
      Status
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        201
      </code>
    </td>
    
    <td>
      The deployment started. The body holds its <code>
        id
      </code>
      
       and its <code>
        status
      </code>
      
      : <code>
        queued
      </code>
      
      , <code>
        building
      </code>
      
       or <code>
        deploying
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        500
      </code>
    </td>
    
    <td>
      The deployment failed to start.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        400
      </code>
    </td>
    
    <td>
      The URL is incomplete or mistyped, for example a character got lost when copying it.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        404
      </code>
    </td>
    
    <td>
      No service has this deploy hook. The URL may have been regenerated.
    </td>
  </tr>
</tbody>
</table>

`curl --fail` exits with an error on any status from 400 up, so your pipeline fails when the deployment can't start.

```json
{ "id": "019a8f3c-5b2d-7e41-9c3a-2d4b6e8f0a1c", "status": "building" }
```

<note>

The hook answers as soon as the deployment has started. It doesn't wait for the build or the rollout to finish. Follow those on the service's **Deployments** tab, where hook deployments show **Deploy hook** under **Started by**.

</note>

## Several calls in a row

The hook follows the same rules as the **Deploy** button: a service runs one deployment at a time, and a newer queued deployment replaces an older one. Since every build takes the newest commit of the branch, several pushes in a row end on the newest commit.

The request body is ignored, and only `POST` deploys. Opening the URL in a browser doesn't.

## Regenerate the URL

If the URL leaked, click **Regenerate** on the **Deploy hook** card and confirm. The old URL stops working right away, so update the secret in your pipelines.

<note>

The URL starts with Harblet's address. After you give Harblet [its own domain](https://harblet.com/docs/administration/harblet-domain), copy the new URL into your pipelines. The old URL with the server's IP address keeps working only while port 3000 is open.

</note>


## Sitemap

See the full [sitemap](https://harblet.com/sitemap.md) for all pages.
