HookDeploy
Advanced

CI/CD Webhook Testing with the HookDeploy REST API

Use the HookDeploy REST API in CI/CD pipelines to create endpoints, send test webhooks, verify capture, and clean up — with a GitHub Actions example.

HookDeploy Team · May 22, 2026

Manual webhook testing does not scale. This guide shows how to automate webhook capture verification in CI using the HookDeploy REST API at api.hookdeploy.dev/v1.

The CI testing pattern

  1. Create a temporary HookDeploy endpoint via API
  2. Send a test webhook to the endpoint URL
  3. Poll the API until the request appears (or timeout)
  4. Assert on captured metadata (method, content type, body size)
  5. Delete the endpoint (cleanup)

This validates your webhook ingestion pipeline without touching production endpoints or manual dashboard checks.

Prerequisites

  • HookDeploy account with an API key (hd_live_...)
  • API key created at Settings → API Keys in your organization
  • CI environment variable: HOOKDEPLOY_API_KEY

Step 1: Create an endpoint

ENDPOINT=$(curl -s -X POST "https://api.hookdeploy.dev/v1/endpoints" \
  -H "Authorization: Bearer $HOOKDEPLOY_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"name\": \"CI test $(date +%s)\"}")

ENDPOINT_ID=$(echo "$ENDPOINT" | jq -r '.data.id')
SLUG=$(echo "$ENDPOINT" | jq -r '.data.slug')

echo "Created endpoint: $ENDPOINT_ID (slug: $SLUG)"

Step 2: Send a test webhook

curl -s -X POST "https://hookdeploy.dev/h/$SLUG" \
  -H "Content-Type: application/json" \
  -d '{"source": "ci", "test_id": "run-12345", "timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}'

Expected response:

{"request_id": "550e8400-e29b-41d4-a716-446655440000"}

Step 3: Verify capture

Poll until the request appears (webhook ingestion is near-instant but allow a few seconds):

MAX_ATTEMPTS=10
ATTEMPT=0
FOUND=false

while [ "$ATTEMPT" -lt "$MAX_ATTEMPTS" ]; do
  REQUESTS=$(curl -s "https://api.hookdeploy.dev/v1/endpoints/$ENDPOINT_ID/requests?limit=1" \
    -H "Authorization: Bearer $HOOKDEPLOY_API_KEY")

  COUNT=$(echo "$REQUESTS" | jq '.data | length')

  if [ "$COUNT" -gt 0 ]; then
    METHOD=$(echo "$REQUESTS" | jq -r '.data[0].method')
    CONTENT_TYPE=$(echo "$REQUESTS" | jq -r '.data[0].content_type')
    BODY_SIZE=$(echo "$REQUESTS" | jq -r '.data[0].body_size_bytes')

    echo "Captured: method=$METHOD content_type=$CONTENT_TYPE body_size=$BODY_SIZE"
    FOUND=true
    break
  fi

  ATTEMPT=$((ATTEMPT + 1))
  sleep 2
done

if [ "$FOUND" = false ]; then
  echo "FAIL: webhook not captured after $MAX_ATTEMPTS attempts"
  exit 1
fi

Step 4: Cleanup

curl -s -X DELETE "https://api.hookdeploy.dev/v1/endpoints/$ENDPOINT_ID" \
  -H "Authorization: Bearer $HOOKDEPLOY_API_KEY"

echo "Deleted endpoint $ENDPOINT_ID"

Always clean up CI endpoints to stay within plan limits.

GitHub Actions example

name: Webhook capture test

on:
  push:
    branches: [main]
  pull_request:

jobs:
  webhook-test:
    runs-on: ubuntu-latest
    steps:
      - name: Create HookDeploy endpoint
        id: create
        env:
          HOOKDEPLOY_API_KEY: ${{ secrets.HOOKDEPLOY_API_KEY }}
        run: |
          RESPONSE=$(curl -s -X POST "https://api.hookdeploy.dev/v1/endpoints" \
            -H "Authorization: Bearer $HOOKDEPLOY_API_KEY" \
            -H "Content-Type: application/json" \
            -d "{\"name\": \"CI ${{ github.run_id }}\"}")

          echo "endpoint_id=$(echo $RESPONSE | jq -r '.data.id')" >> $GITHUB_OUTPUT
          echo "slug=$(echo $RESPONSE | jq -r '.data.slug')" >> $GITHUB_OUTPUT

      - name: Send test webhook
        run: |
          curl -sf -X POST "https://hookdeploy.dev/h/${{ steps.create.outputs.slug }}" \
            -H "Content-Type: application/json" \
            -d "{\"run_id\": \"${{ github.run_id }}\", \"ref\": \"${{ github.ref }}\"}"

      - name: Verify capture
        env:
          HOOKDEPLOY_API_KEY: ${{ secrets.HOOKDEPLOY_API_KEY }}
        run: |
          for i in $(seq 1 10); do
            REQUESTS=$(curl -s \
              "https://api.hookdeploy.dev/v1/endpoints/${{ steps.create.outputs.endpoint_id }}/requests?limit=1" \
              -H "Authorization: Bearer $HOOKDEPLOY_API_KEY")

            if [ "$(echo $REQUESTS | jq '.data | length')" -gt 0 ]; then
              echo "Webhook captured successfully"
              echo "$REQUESTS" | jq '.data[0]'
              exit 0
            fi
            sleep 2
          done
          echo "Webhook not captured"
          exit 1

      - name: Delete endpoint
        if: always()
        env:
          HOOKDEPLOY_API_KEY: ${{ secrets.HOOKDEPLOY_API_KEY }}
        run: |
          curl -s -X DELETE \
            "https://api.hookdeploy.dev/v1/endpoints/${{ steps.create.outputs.endpoint_id }}" \
            -H "Authorization: Bearer $HOOKDEPLOY_API_KEY"

Add HOOKDEPLOY_API_KEY to your repository secrets.

Testing your application’s webhook handler

Extend the pattern to test your full pipeline:

  1. Create HookDeploy endpoint with forward URL pointing to your CI-deployed staging app
  2. Send test webhook to HookDeploy
  3. Verify your app received and processed it (check app logs or a status endpoint)
  4. Cleanup

Or use replay API after deploy:

REQUEST_ID=$(echo "$REQUESTS" | jq -r '.data[0].id')

curl -s -X POST \
  "https://api.hookdeploy.dev/v1/endpoints/$ENDPOINT_ID/requests/$REQUEST_ID/replay" \
  -H "Authorization: Bearer $HOOKDEPLOY_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"target_url\": \"https://staging.example.com/webhooks/test\"}"

Rate limits in CI

API rate limits apply per key:

PlanRequests/minute
Free60
Starter120
Team300

A typical CI run uses 4–5 API calls (create, list, delete). Well within limits unless you parallelize hundreds of jobs.

Check usage

Monitor endpoint count to avoid hitting plan limits from orphaned CI endpoints:

curl -s "https://api.hookdeploy.dev/v1/usage" \
  -H "Authorization: Bearer $HOOKDEPLOY_API_KEY" | jq '.data'

Use if: always() on cleanup steps — orphaned endpoints accumulate if CI fails mid-run.

Related guides