Skip to main content

Run Replay Cases in CI

Replay cases can run from any CI system that can make an authenticated HTTP request. This lets you turn real webhook payloads captured in Hooklistener into regression checks for staging, preview, or production-like webhook handlers.

The examples below use GitHub Actions, but the same API works in GitLab CI, CircleCI, Buildkite, Jenkins, or a local script.

What the CI job does

A typical CI workflow:

  1. Deploys or starts a webhook handler that Hooklistener can reach.
  2. Calls the Hooklistener Request Cases API with wait: true.
  3. Fails the CI job when the case run reports failed or timeout.
  4. Prints the case run report URL and aggregate assertion counts.

Use wait: true when you want CI to block until queued replays finish. The wait timeout defaults to 30 seconds and is capped at 120 seconds.

Required secrets

Create these secrets in your CI provider:

SecretDescription
HOOKLISTENER_API_KEYOrganization API key from Organization Settings > API Keys
HOOKLISTENER_ENDPOINT_IDDebug endpoint ID that owns the saved cases
HOOKLISTENER_TARGET_URLPublic URL that should receive each replayed webhook

The target URL must be reachable from Hooklistener's servers. For hosted CI, use a deployed preview URL, staging URL, or tunnel URL rather than localhost.

GitHub Actions workflow

Save this as .github/workflows/hooklistener-replay-cases.yml in the application repo that owns your webhook handler:

name: Hooklistener replay cases

on:
pull_request:
workflow_dispatch:

jobs:
replay-cases:
runs-on: ubuntu-latest
env:
HOOKLISTENER_API_KEY: ${{ secrets.HOOKLISTENER_API_KEY }}
HOOKLISTENER_ENDPOINT_ID: ${{ secrets.HOOKLISTENER_ENDPOINT_ID }}
HOOKLISTENER_TARGET_URL: ${{ secrets.HOOKLISTENER_TARGET_URL }}
steps:
- name: Run saved replay cases
shell: bash
run: |
set -euo pipefail

response_file="$(mktemp)"
request_body="$(jq -n \
--arg target_url "$HOOKLISTENER_TARGET_URL" \
'{
target_url: $target_url,
target_name: "GitHub Actions",
wait: true,
timeout_ms: 120000
}')"

status_code="$(
curl --silent --show-error --location \
--output "$response_file" \
--write-out "%{http_code}" \
--request POST "https://app.hooklistener.com/api/v1/endpoints/${HOOKLISTENER_ENDPOINT_ID}/cases/run" \
--header "Authorization: Bearer ${HOOKLISTENER_API_KEY}" \
--header "Content-Type: application/json" \
--data "$request_body"
)"

cat "$response_file"
echo

if [[ "$status_code" -lt 200 || "$status_code" -ge 300 ]]; then
echo "Hooklistener API returned HTTP $status_code"
exit 1
fi

result_status="$(jq -r '.data.result_status // "unknown"' "$response_file")"
report_url="$(jq -r '.data.case_suite_run_url // empty' "$response_file")"
queued_count="$(jq -r '.data.queued_count // 0' "$response_file")"
failed_count="$(jq -r '.data.failed_count // 0' "$response_file")"
passed_count="$(jq -r '.data.passed_count // 0' "$response_file")"
assertion_failed_count="$(jq -r '.data.assertion_failed_count // 0' "$response_file")"
assertion_error_count="$(jq -r '.data.assertion_error_count // 0' "$response_file")"

echo "Hooklistener result: $result_status"
echo "Queued cases: $queued_count"
echo "Queue failures: $failed_count"
echo "Passed assertions: $passed_count"
echo "Failed assertions: $assertion_failed_count"
echo "Assertion errors: $assertion_error_count"

if [[ -n "$report_url" ]]; then
echo "Report: https://app.hooklistener.com${report_url}"
fi

case "$result_status" in
passed|completed)
exit 0
;;
failed|timeout)
exit 1
;;
*)
echo "Unexpected Hooklistener result_status: $result_status"
exit 1
;;
esac

completed means all deliveries finished but no replay assertions were configured. Add assertions to your saved cases when you want CI to verify response status or response body fields.

Run one named suite

To run only a named suite, add a HOOKLISTENER_CASE_SUITE_ID secret:

env:
HOOKLISTENER_API_KEY: ${{ secrets.HOOKLISTENER_API_KEY }}
HOOKLISTENER_ENDPOINT_ID: ${{ secrets.HOOKLISTENER_ENDPOINT_ID }}
HOOKLISTENER_CASE_SUITE_ID: ${{ secrets.HOOKLISTENER_CASE_SUITE_ID }}
HOOKLISTENER_TARGET_URL: ${{ secrets.HOOKLISTENER_TARGET_URL }}

Then replace the request_body assignment in the workflow with:

request_body="$(jq -n \
--arg case_suite_id "$HOOKLISTENER_CASE_SUITE_ID" \
--arg target_url "$HOOKLISTENER_TARGET_URL" \
'{
case_suite_id: $case_suite_id,
target_url: $target_url,
target_name: "GitHub Actions",
wait: true,
timeout_ms: 120000
}')"

Use suites for focused checks such as Billing webhooks, GitHub issue lifecycle, or Shopify order events.

Use a saved replay target

If your endpoint already has a saved replay target, pass target_id instead of target_url:

request_body="$(jq -n \
--arg target_id "$HOOKLISTENER_TARGET_ID" \
'{
target_id: $target_id,
wait: true,
timeout_ms: 120000
}')"

curl --request POST "https://app.hooklistener.com/api/v1/endpoints/${HOOKLISTENER_ENDPOINT_ID}/cases/run" \
--header "Authorization: Bearer ${HOOKLISTENER_API_KEY}" \
--header "Content-Type: application/json" \
--data "$request_body"

Saved targets are useful when the same staging or preview URL is reused across workflows.

Generic shell script

For CI systems that do not use GitHub Actions, the core command is:

request_body="$(jq -n \
--arg target_url "$HOOKLISTENER_TARGET_URL" \
'{
target_url: $target_url,
wait: true,
timeout_ms: 120000
}')"

response="$(
curl --fail-with-body --silent --show-error \
--request POST "https://app.hooklistener.com/api/v1/endpoints/${HOOKLISTENER_ENDPOINT_ID}/cases/run" \
--header "Authorization: Bearer ${HOOKLISTENER_API_KEY}" \
--header "Content-Type: application/json" \
--data "$request_body"
)"

result_status="$(jq -r '.data.result_status' <<<"$response")"

case "$result_status" in
passed|completed)
exit 0
;;
failed|timeout)
exit 1
;;
*)
echo "$response"
exit 1
;;
esac

Troubleshooting

The run returns No saved cases exist for this endpoint - save at least one captured request as a replay case before running CI.

The job passes with result_status: completed but does not verify anything - the cases delivered successfully, but no assertions were configured. Add expected status codes or expected JSON body subsets to the saved cases.

The run returns timeout - the cases were queued, but not all forwards completed before timeout_ms. Increase the timeout up to 120000, or inspect the report URL to see which forward is still waiting.

The target never receives a request - make sure HOOKLISTENER_TARGET_URL is publicly reachable from Hooklistener. A local development URL such as http://localhost:3000/webhooks will only work when replaying through an active CLI listener, not from hosted CI.

The API returns 401 or 403 - verify that HOOKLISTENER_API_KEY starts with hklst_, belongs to the same organization as the endpoint, and has not been revoked.