Test Webhooks Locally: Tunnels, Signing & Message Log

How to Test Webhooks Locally (Tunnels + Signed Requests + Message Log)

Testing webhooks locally is essential for debugging integrations without deploying to production. This guide shows how to set up a reliable local testing loop using tunnels, signed requests, and the Message Log to verify webhook deliveries. It includes examples to postman send signed webhook request hmac timestamp for local verification.

Reference implementation: SendPromptly Webhooks reference kit (GitHub Pages)

The reliable local testing loop

Tunnel → webhook endpoint → signature verify → ack 200 → async job

The testing loop involves:

  1. Exposing your local server via a tunnel.
  2. Receiving webhook requests at your endpoint.
  3. Verifying signatures.
  4. Acknowledging with a 200 response.
  5. Processing the payload asynchronously.

This ensures your endpoint behaves like production.

Use Message Log as source of truth for attempts/outcomes

The Message Log provides a complete view of delivery attempts, including successes, failures, and retries. Use it to confirm your local setup works.

Learn more about delivery rules and retries.

Common gotcha: Always check the Message Log after triggering a test event, as it shows the full delivery history.

Tunnel options

ngrok (quick), Cloudflare Tunnel (stable), localtunnel (simple)

Choose a tunnel based on your needs:

  • ngrok: Quick setup, but URLs change on restart.
  • Cloudflare Tunnel: Stable, integrates with Cloudflare for security.
  • localtunnel: Simple, no account required.

Suggested diagram: A comparison table of tunnel options (e.g., setup time, stability, cost).

Hook your tunnel into SendPromptly

Create webhook endpoint in your project/environment

Configure your SendPromptly project to send webhooks to your tunnel URL. For example, set the webhook URL to https://your-ngrok-url.ngrok.io/webhooks/sendpromptly.

To trigger a real delivery from the live API use: POST https://app.sendpromptly.com/api/v1/events (include your Authorization: Bearer <token> and an Idempotency-Key).

Trigger a test event via API curl

Use the Getting Started curl to trigger a real event and test your endpoint.

Trigger a real event and confirm delivery runs.

Use Sample Project to generate a real delivery and verify the run in Message Log.

Micro checklist:

  • Confirm tunnel URL is configured in SendPromptly webhook settings
  • Trigger a test event (Getting Started curl)
  • Watch Message Log for the delivery attempt and status

Postman/curl signed requests (when you don’t want to trigger SendPromptly)

Generate signature locally

To test signatures without triggering SendPromptly, generate the signature locally using your secret. This is the same flow used to postman send signed webhook request hmac timestamp when validating locally.

Send to localhost endpoint

Send the signed request to your local endpoint using Postman or curl.

1
2
3
4
5
Route::post('/webhooks/sendpromptly', function (\Illuminate\Http\Request $r) {
    // signature middleware runs first
    dispatch(new \App\Jobs\ProcessSendPromptlyWebhook($r->json()->all()));
    return response()->json(['ok' => true], 200);
});

Minimal test snippet:

1
2
3
4
5
6
7
8
9
TS=$(date +%s)
BODY='{"event_key":"order.created","payload":{"order_id":"O-1001"}}'
SIG=$(php -r '$ts=getenv("TS");$b=getenv("BODY");$s=getenv("SECRET");echo hash_hmac("sha256",$ts.".".$b,$s);' \
  TS="$TS" BODY="$BODY" SECRET="replace_me")
curl -i -X POST "http://localhost:8000/webhooks/sendpromptly" \
  -H "Content-Type: application/json" \
  -H "X-SP-Timestamp: $TS" \
  -H "X-SP-Signature: v1=$SIG" \\
  --data "$BODY"

Learn more about signed local requests.

Debug playbook

Missing headers

Check that all required headers (X-SP-Timestamp, X-SP-Signature) are present in the request.

Signature mismatch

Verify you’re using the raw body and correct secret. Compare computed and received signatures.

Timeouts

Ensure your endpoint responds quickly (under 10 seconds) to avoid retries.

Micro checklist:

  • Confirm tunnel is running and URL is correct.
  • Verify signature generation uses raw body.
  • Check for firewall blocks.
  • Monitor response times.

Common failure modes

  1. Tunnel URL changed (ngrok restart) → deliveries fail.
  2. HTTPS/TLS mismatch → connection errors.
  3. Signature headers missing → verification rejects (401). ([sendpromptly.com][1])
  4. Local app too slow → timeouts → retries.
  5. Middleware signs the wrong bytes (decoded JSON) → mismatch.
  6. Firewall blocks inbound to local dev.

Learn more about debug workflow using Message Log.

Conclusion

When it fails, open Message Log → inspect status/latency → adjust endpoint until it’s consistently 2xx.

Key takeaways

  • Use tunnels to expose localhost for testing.
  • Verify signatures using raw body and constant-time comparisons.
  • Trigger real events via SendPromptly and check Message Log.
  • Debug with signed requests in Postman or curl.
  • Monitor for common failures like timeouts and header mismatches.