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
- Create a temporary HookDeploy endpoint via API
- Send a test webhook to the endpoint URL
- Poll the API until the request appears (or timeout)
- Assert on captured metadata (method, content type, body size)
- 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:
- Create HookDeploy endpoint with forward URL pointing to your CI-deployed staging app
- Send test webhook to HookDeploy
- Verify your app received and processed it (check app logs or a status endpoint)
- 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:
| Plan | Requests/minute |
|---|---|
| Free | 60 |
| Starter | 120 |
| Team | 300 |
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
Related guides
- Local webhook testing — four approaches compared
Compare ngrok, Cloudflare Tunnel, HookDeploy forwarding, and VS Code port forwarding for getting webhooks to your local dev server.
- Webhook debugging guide
Systematic approach to debugging webhook issues — delivery logs, signatures, payloads, curl testing, and common HTTP error codes.
- Testing Stripe webhooks with HookDeploy
Complete guide to capturing, inspecting, and debugging Stripe webhook events using HookDeploy, the Stripe CLI, and signature verification.